Merge pull request #56 from pengzhanbo/new_docs

全新的文档!
This commit is contained in:
pengzhanbo 2024-03-09 00:38:32 +08:00 committed by GitHub
commit c2daeaae6a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
190 changed files with 5818 additions and 10372 deletions

View File

@ -9,7 +9,7 @@
"files.trimTrailingWhitespace": false
},
"[typescript]": {
"editor.defaultFormatter": "Vue.volar"
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"css.validate": false,
"scss.validate": false,

View File

@ -9,10 +9,15 @@ export default defineUserConfig({
source: path.resolve(__dirname, '../'),
public: path.resolve(__dirname, 'public'),
locales: {
'/': { title: 'Plume主题', description: '', lang: 'zh-CN' },
'/en/': { title: 'Plume Theme', description: '', lang: 'en-US' },
'/': { title: 'Plume 主题', lang: 'zh-CN' },
'/en/': { title: 'Plume Theme', lang: 'en-US' },
},
head: [
['link', { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-32x32.png' }],
['link', { rel: 'icon', type: 'image/png', sizes: '16x16', href: '/favicon-16x16.png' }],
],
bundler: viteBundler(),
theme,

View File

@ -1,64 +1,84 @@
import type { NavItem } from 'vuepress-theme-plume'
import { version } from '../../package.json'
export const zhNavbar = [
{ text: '首页', link: '/', icon: 'material-symbols:home-outline' },
{
text: '指南',
icon: 'icon-park-outline:guide-board',
link: '/guide/intro/',
activeMatch: '^/guide/',
},
{
text: '配置',
icon: 'icon-park-outline:setting-two',
link: '/config/intro/',
activeMatch: '^/config/',
},
{
text: '博客',
link: '/blog/',
icon: 'material-symbols:article-outline',
activeMatch: '^/(blog|article)/',
},
{
text: 'VuePress',
icon: 'vscode-icons:file-type-vue',
text: '更多',
items: [
{
text: 'theme-plume',
link: '/note/vuepress-theme-plume/',
icon: 'icon-park-outline:theme',
},
{
text: '插件',
icon: 'mingcute:plugin-2-line',
text: 'Vuepress',
items: [
{
text: 'caniuse',
link: '/note/vuepress-plugin/caniuse/',
icon: 'tabler:brand-css3',
},
{
text: 'netlify-functions',
link: '/note/vuepress-plugin/netlify-functions/',
icon: 'teenyicons:netlify-outline',
},
{ text: '官方文档', link: 'https://v2.vuepress.vuejs.org' },
{ text: '生态系统', link: 'https://ecosystem.vuejs.press/' },
],
},
],
},
{ text: '友情链接', link: '/friends/', icon: 'emojione-monotone:roller-coaster' },
{
text: `${version}`,
icon: 'codicon:versions',
items: [
{ text: '更新日志', link: 'https://github.com/pengzhanbo/vuepress-theme-plume/blob/main/CHANGELOG.md' },
{ text: '参与贡献', link: 'https://github.com/pengzhanbo/vuepress-theme-plume/blob/main/CONTRIBUTING.md' },
],
},
] as NavItem[]
export const enNavbar = [
{ text: 'Home', link: '/en/', icon: 'material-symbols:home-outline' },
{
text: 'Guide',
icon: 'icon-park-outline:guide-board',
link: '/en/guide/intro/',
activeMatch: '^/en/guide/',
},
{
text: 'Config',
icon: 'icon-park-outline:setting-two',
link: '/en/config/intro/',
activeMatch: '^/en/config/',
},
{
text: 'Blog',
link: '/en/blog/',
icon: 'material-symbols:article-outline',
activeMatch: '^/en/(blog|article)/',
},
{
text: 'VuePress',
icon: 'vscode-icons:file-type-vue',
text: 'More',
items: [
{
text: 'Plugin',
icon: 'mingcute:plugin-2-line',
text: 'Vuepress',
items: [
{
text: 'caniuse',
link: '/en/note/vuepress-plugin/caniuse/',
icon: 'tabler:brand-css3',
},
{ text: 'Official Docs', link: 'https://v2.vuepress.vuejs.org' },
{ text: 'Ecosystem', link: 'https://ecosystem.vuejs.press/' },
],
},
],
},
{
text: `${version}`,
icon: 'codicon:versions',
items: [
{ text: 'Changelog', link: 'https://github.com/pengzhanbo/vuepress-theme-plume/blob/main/CHANGELOG.md' },
{ text: 'Contributing', link: 'https://github.com/pengzhanbo/vuepress-theme-plume/blob/main/CONTRIBUTING.md' },
],
},
] as NavItem[]

View File

@ -2,45 +2,72 @@ import { definePlumeNotesConfig } from 'vuepress-theme-plume'
export const zhNotes = definePlumeNotesConfig({
dir: 'notes',
link: '/note',
link: '/',
notes: [
{
dir: 'vuepress-theme-plume',
link: '/vuepress-theme-plume/',
dir: 'theme/guide',
link: '/guide/',
sidebar: [
'',
{
text: '指南',
icon: 'icon-park-outline:guide-board',
items: ['快速开始', '编写文章'],
text: '快速开始',
collapsed: false,
items: ['介绍', '安装与使用', '博客', '知识笔记'],
},
{
text: '配置',
text: '写作',
collapsed: false,
items: [
'编写文章',
{
text: '主题配置',
link: '主题配置',
items: ['主题插件配置', 'notes配置'],
text: 'markdown',
items: ['markdown-基础', 'markdown-扩展', 'markdown-进阶', 'markdown-试验性'],
},
'页面配置',
'国际化',
],
},
{
text: '功能',
items: ['基础功能', 'markdown增强'],
collapsed: false,
items: ['代码复制', '内容搜索', '评论', '加密', '组件', '友情链接页', 'seo', 'sitemap'],
},
{
text: '自定义',
collapsed: false,
items: ['自定义首页', '自定义样式'],
},
{
text: 'API',
collapsed: false,
items: ['api-客户端', 'api-node'],
},
],
},
{
dir: 'vuepress-plugin',
link: '/vuepress-plugin/',
dir: 'theme/config',
link: '/config/',
sidebar: [
'caniuse/README',
{
dir: 'netlify-functions',
text: 'plugin-netlify-functions',
link: 'netlify-functions',
items: ['', '介绍', '使用', '功能', 'API', 'functions开发指南'],
text: '配置',
collapsed: false,
items: [
'配置说明',
'多语言配置',
'主题配置',
'导航栏配置',
'notes配置',
],
},
{
text: 'frontmatter',
dir: 'frontmatter',
collapsed: false,
items: ['basic', 'home', 'post', 'friend'],
},
{
text: '内置插件',
dir: 'plugins',
collapsed: false,
items: ['', '代码复制', '代码高亮', '搜索', '阅读统计', 'markdown增强', '百度统计'],
},
],
},
@ -49,12 +76,6 @@ export const zhNotes = definePlumeNotesConfig({
export const enNotes = definePlumeNotesConfig({
dir: 'en/notes',
link: '/en/note',
notes: [
{
dir: 'vuepress-plugin',
link: '/vuepress-plugin/',
sidebar: ['caniuse/README'],
},
],
link: '/',
notes: [],
})

View File

@ -1 +1 @@
pengzhanbo.cn
plume.pengzhanbo.cn

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 694 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 949 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 218 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 405 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -4,20 +4,20 @@ import { enNotes, zhNotes } from './notes.js'
import { enNavbar, zhNavbar } from './navbar.js'
export const theme: Theme = themePlume({
logo: 'https://pengzhanbo.cn/g.gif',
hostname: 'https://pengzhanbo.cn',
logo: '/plume.png',
hostname: 'https://plume.pengzhanbo.cn',
repo: 'https://github.com/pengzhanbo/vuepress-theme-plume',
docsDir: 'docs',
avatar: {
url: '/images/blogger.jpg',
url: '/plume.png',
name: 'Plume Theme',
description: 'The Theme for Vuepress 2.0',
},
social: [{ icon: 'github', link: 'https://github.com/pengzhanbo' }],
social: [{ icon: 'github', link: 'https://github.com/pengzhanbo/vuepress-theme-plume' }],
footer: { copyright: 'Copyright © 2022-present pengzhanbo' },
footer: { copyright: 'Copyright © 2021-present pengzhanbo' },
locales: {
'/': {
@ -31,5 +31,27 @@ export const theme: Theme = themePlume({
},
plugins: {
shiki: { twoslash: true },
markdownEnhance: {
demo: true,
},
comment: {
provider: 'Giscus',
comment: true,
repo: 'pengzhanbo/vuepress-theme-plume',
repoId: 'R_kgDOG_ebNA',
category: 'docs-comment',
categoryId: 'DIC_kwDOG_ebNM4Cd0uF',
mapping: 'url',
reactionsEnabled: true,
inputPosition: 'top',
darkTheme: 'dark_protanopia',
lightTheme: 'light_protanopia',
},
},
encrypt: {
rules: {
'/article/enx7c9s/': '123456',
},
},
})

View File

@ -1,36 +0,0 @@
---
title: BFC 块级格式化上下文
createTime: 2018/05/17 12:28:33
permalink: /article/o5g7ggvf/
author: pengzhanbo
tags:
- html
---
## 概念
BFC, Block Formatting Context。是 W3C CSS2.1规范中的一个概念。 是页面中的一块块级渲染区域,并且有一套渲染规则,它决定了其子元素将如何定位,以及和其他元素的关系和作用。
具有BFC特性的元素可以看做是一个被隔离了的独立容器容器内的元素不会在布局上影响到外面的元素并且BFC具有普通容器所没有的一些特性。
## 创建BFC的方式
1. 根元素html
2. 浮动元素,即 float值不为 none。
3. 绝对定位元素, 元素的 position 为 absolute 或者 fixed
4. 行内块元素, 元素的 display 为 inline-block
5. 表格单元格, 元素的 display 为 table-cell。 HTML表格单元格默认为该值
6. 表格标题, 元素的display为table-caption。 HTML表格标题默认为该值
7. 匿名表格单元格元素, 元素的display为 table、table-row、table-row-group、table-header-group、table-footer-group 。 (分别是 HTML table、row、tbody、thead、tfoot的默认属性或 inline-table。
8. overflow计算值不为visible的块元素
9. display值为 flow-root的元素
10. contain值为 layout、content、paint的元素
11. 弹性元素display为 flex、inline-flex元素的直接子元素
12. 网格元素, display为gird、inline-gird元素的直接子元素
13. 多列容器元素的column-count或column-width不为 auto 包括column-count不为1
14. colum-span为all的元素始终会创建一个新的BFC即使该元素没有包裹在一个多列容器中。
## 作用
1. 同一个BFC的外边距会发生折叠合并 通过将其放在不同的BFC中规避折叠。
2. BFC可以包含浮动元素即清除浮动。
3. BFC可以阻止元素被浮动元素覆盖。

View File

@ -1,161 +0,0 @@
---
title: CSS At-Rules
createTime: 2018/10/06 08:16:38
permalink: /article/btkqop1a/
author: pengzhanbo
tags:
- css
---
## @charset
#### 概述
指定样式表中使用的字符编码。 它必须位于样式表中的第一个元素,且前面不得有任何字符。
不能在 `<style>` 元素内的样式属性内使用。
#### 示例:
``` css
@charset "UTF-8";
@charset "utf-8"; /*大小写不敏感*/
/* 设置css的编码格式为Unicode UTF-8 */
@charset 'UTF-8'; /* 无效的, 使用了错误的引号 */
@charset "UTF-8"; /* 无效的, 多于一个空格 */
@charset "UTF-8"; /* 无效的, 在at-rule之前多了一个空格 */
@charset UTF-8; /* Invalid, without ' or ", the charset is not a CSS <string> */
```
## @font-face
#### 概述
指定一个用于显示文本的自定义字体。
字体可以从远程服务器,也可以是用户本地安装的字体。
`@font-face` 可以解除对用户电脑字体的依赖。
#### 语法
``` css
@font-face {
[ font-family: <family-name>; ] ||
[ src: <src>; ] ||
[ unicode-range: <unicode-range>; ] ||
[ font-variant: <font-variant>; ] ||
[ font-feature-settings: <font-feature-settings>; ] ||
[ font-variation-settings: <font-variation-settings>; ] ||
[ font-stretch: <font-stretch>; ] ||
[ font-weight: <font-weight>; ] ||
[ font-style: <font-style>; ] ||
[ size-adjust: <size-adjust>; ] ||
[ ascent-override: <ascent-override>; ] ||
[ descent-override: <descent-override>; ] ||
[ line-gap-override: <line-gap-override>; ]
}
```
- `font-family`: 指定的 `<family-name>` 将会被用于 `font``font-family`的属性
- `src`: 远程字体文件的位置,或者通过`local`函数通过字体名字从本地加载字体。
#### 使用示例:
加载远程字体文件:
``` html
...
<style>
@font-face {
font-family: custom-font;
src: url("http://example.com/custom-font.ttf")
}
body {
font-family: custom-font;
}
</style>
...
```
加载字体文件,先尝试从用户本地加载,如果加载失败则从远程服务器下载:
``` html
...
<style>
@font-face {
font-family: MgOpenModernaBold;
src: local("Helvetica Neue Bold"),
url(MgOpenModernaBold.ttf);
}
body {
font-family: MgOpenModernaBold;
}
</style>
...
```
加载不同文件格式的字体,根据用户环境判断使用兼容的字体文件格式:
``` html
...
<style>
@font-face {
font-family: custom;
src: url("custom.ttf") format("tff"),
url("custom.woff") format("woff"),
url("custom.woff2") format("woff2");
}
body {
font-family: custom;
}
</style>
...
```
## @import
#### 概述
从其他样式表导入样式规则。
`@import` 必须优先于其他类型的规则,即需要在文件顶部声明。`@charset` 除外。
#### 语法
``` css
@import url;
@import url list-of-media-queries;
```
- `url` 样式规则文件资源位置
- `list-of-media-queries` [媒体查询](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries),支持用逗号分隔多个查询条件。资源仅在满足媒体查询条件时才会被加载。
## @keyframes
#### 概述
通过在动画序列中定义关键帧的样式来控制CSS动画序列中的中间步骤。
#### 示例
使用 `from`,`to` 定义起始和结束关键帧的样式 实现动画
``` css
@keyframes slidein {
from {
transform: translateX(0%);
}
to {
transform: translateX(100%);
}
}
```
使用 百分比 定义触发关键帧的时间点
``` css
@keyframes slidein {
0% {
transform: translateX(0%);
}
50% {
transform: translateX(50%);
}
100% {
transform: translateX(100%);
}
}
```
## @media
媒体查询,详见 [CSS @media 媒体查询](/post/fe5ruia1/)

View File

@ -1,190 +0,0 @@
---
title: CSS 媒体查询
createTime: 2018/08/18 08:43:02
permalink: /article/fe5ruia1/
author: pengzhanbo
tags:
- css
---
开发响应式网站时,常常需要使用到 media 媒体查询。这里总结下媒体查询的使用方法。
## 概述
媒体查询是通过判断当前媒体是否满足 媒体查询规则,从而使其包含的 CSS规则生效。
从 CSS level 2 开始,就已经支持 `media-queries`,到 CSS level 3 以及之后的版本,媒体查询变得更加的丰富和能够适应更多的场景。
## 使用
媒体查询可以通过以下三种方式进行使用:
#### 在 `<link>` 元素引入CSS资源时声明 `media` 属性
``` html
<link rel="stylesheet" type="text/css" href="media/custom.css" media="screen and (min-width: 400px)">
```
#### 在`<style>` 上 声明 `media` 属性
``` html
<style media="screen and (min-width: 400px)">
</style>
```
#### 在`@import` 后 声明 媒体查询条件
``` css
@import url('custom.css') screen and (min-width: 400px);
```
#### 在样式表中使用 At-Rule `@media` 使用媒体查询规则
``` css
@media screen and (min-width: 400px) {
.example {
color: red;
}
}
```
### 语法
``` html
<link rel="stylesheet" type="text/css" href="media/custom.css" media="[media-queries-list]">
<style media="[media-queries-list]">
</style>
<style>
@import url [media-queries-list];
@media [media-queries-list] {
<style-sheet-group>
}
</style>
```
## 媒体查询 [media-queries-list]
`media-queries-list` 可以由以下三种内容组成:
- `Media types` :媒体类型, 表示设备
- `Media features` :媒体特性, 表示设备的状态
- `Logical operators` 逻辑操作符, 连接多个 `media-query`
### Media types
`Media types` 描述设备的一般类型。可以使用以下值:
- `all`: 表示适用于所有设备。 默认值。
- `print`: 表示 适用于在屏幕上以打印预览的模式查看页面和文档。
- `screen`: 表示 适用于屏幕 。
> 在 *css2.1**Media Queries 3* 中还支持 `tty``tv``projection``handheld``braille``embossed``aural`,但这些值都已经在*Media Queries 4* 中被弃用。
### Media features
媒体特性,描述 用户代理、输出设备以及环境的特定特征。
媒体特性表达式是完全是可选的,并且负责测试这些特性是否存在,值为多少。 且每个媒体特性表达式都必须使用括号括起来。
*以下仅列出比较常用到的媒体特性*
- `width`: 视窗viewport的宽度包括纵向滚动条的宽度。
值的类型为 number单位可以是 `px``em` 等。
``` css
with: 400px
```
- `height`: 视窗viewport的高度。
值的类型为 number单位可以是 `px``em` 等。
``` css
height: 600px
```
- `aspect-ratio` 视窗viewport的宽高比。
值的类型为 number/number。
``` css
aspect-ratio: 3/2
```
- `orientation` 视窗viewport 的旋转方向。
- portrait 设备竖屏
- landscape 设备横屏
``` css
orientation: landscape
```
- `resolution`: 输出设备的分辨率
值的类型为 number单位为 `dpi`
``` css
resolution: 320dpi
```
- `scan`:输出设备的扫描过程(适用于电视机等)。
#### 媒体特性前缀
大部分的媒体特性均支持前缀,用于约束媒体特性的作用范围。
- `max-[media feature]` 小于指定的最大值时,返回*true*
- `min-[media feature]`: 大于指定的最小值时,返回*true*
*个人认为使用前缀时其表述稍显拗口,建议使用取值范围的方式声明表达式*
#### 媒体特性语法
- 以键值对的形式,表述取固定的值
````
([media-feature-name]: [media-feature-value])
````
- 直接书写name 表示值的结果为 boolean
```
([media-feature-name])
```
- 表述 特性的取值范围
*声明 range 为描述数学符号 : '<' | '>' | '<=' | '>='*
```
([media-feature-name] [range] [media-feature-value])
([media-feature-name] [range] [media-feature-value] [range] [media-feature-value])
```
### Logical operators
逻辑操作符用于组成复合的 media queries。
- `and`: 用于合并多条`media query`, 且 每条 `media query` 均返回 *true* 时,
媒体查询表达式的结果返回*true*。
- `not`: 取反操作,使用`not [media query]`,当`media query` 返回 *false* 时,
媒体查询表达式的结果返回*true*。
- `,`: or操作符组合多个 `media query`,任意一个`media query` 返回 *true*,
媒体查询表达式的结果返回*true*。
- `only`: 不支持更加高级的媒体类型的浏览器检测到only修饰的时候就会抛弃这个规则
## 使用示例详解
### 示例1
``` css
@media screen and (width > 414px) {}
```
当设备的屏幕视窗宽度大于414px时应用CSS块中的样式规则。
### 示例2
``` css
@media (width > 800px), screen and (orientation: landscape) {}
```
当前设备 视窗宽度大于 800px 或者设备方向为横向时应用css块中的样式规则。
### 示例3
``` css
@media screen and (414px < width < 800px) {}
```
当前设备屏幕视窗宽度 大于 414px 且 小于 800px 时, 应用css块中的样式规则。

View File

@ -1,645 +0,0 @@
---
title: CSS选择器
createTime: 2018/09/20 03:29:20
permalink: /article/8vev8ixl/
author: pengzhanbo
tags:
- css
---
## Basic Selectors 基础选择器
### Element selector
根据 element type 匹配 一组元素
``` html
...
<style>
p { color: red; }
</style>
...
<p>content</p>
...
```
### Class selector
根据 element 声明的 class属性值 匹配一组元素
``` html
...
<style>
.red { color: red; }
</style>
...
<p class="red">content</p>
...
```
### ID selector
根据 element 声明的 ID属性值匹配一个元素一个页面中ID具有唯一性
``` html
...
<style>
#red { color: red; }
</style>
...
<p id="red">content</p>
...
```
### Universal selector
通配符,匹配所有 element
``` html
...
<style>
* { color: red; }
</style>
...
<p>content</p>
<span>span</span>
...
```
## Attribute Selectors
### \[attribute\] selector
匹配声明了该attribute的 一组 element
``` html
...
<style>
[href] { color: red; }
</style>
...
<a href="">content</a>
...
```
### \[attribute="x"\] selector
匹配声明了该attribute且值为 x 的一组 element
``` html
...
<style>
[title="a"] { color: red; }
</style>
...
<abbr title="a">abbr</abbr>
...
```
### \[attribute~="x"\] selector
匹配声明了该attribute且值包含了 单词 x 的一组 element
``` html
...
<style>
[title~="style"] { color: red; } /* 匹配包含了 独立单词 style 的 element */
</style>
...
<abbr title="sheet style">abbr</abbr>
<abbr title="sheetstyle"></abbr> <!-- no match -->
...
```
### \[attribute|="x"\] selector
匹配声明了该attribute且值包含了一个 `x-` 开头的连字符拼接的词 的一组 element
``` html
...
<style>
/* lang的值必须 包含 en 通过连接符 - 连接另一个单词的 词 */
[lang|="en"] { color: red; }
</style>
...
<abbr lang="en-US">abbr</abbr>
<!-- no match lang="en" lang="enUS" -->
...
```
### \[attribute^="x"\] selector
匹配声明了该attribute且值是以 x 作为开头的 一组 element
``` html
...
<style>
[href^="https://"] { color: red; }
</style>
...
<a href="https://example.com">content</a>
...
```
### \[attribute$="x"\] selector
匹配声明了该attribute且值是以 x 作为结尾的 一组 element
``` html
...
<style>
[href$=".pdf"] { color: red; }
</style>
...
<a href="https://example.com/a.pdf">content</a>
...
```
### \[attribute*="x"\] selector
匹配声明了该attribute且值包含了子串 x 的 一组 element
``` html
...
<style>
[href*="example"] { color: red; }
</style>
...
<a href="https://example.com">content</a>
...
```
## Combinators 关系选择器
关系选择器适用于 任意选择器 的组合
### selector1 selector2 后代关系选择器
匹配 selector1 的元素中,所有 selector2 的 元素
``` html
...
<style>
section span { color: red; }
</style>
...
<section>
<span></span> <!-- match -->
<p><span></span></p> <!-- match -->
</section>
...
```
### selector1 > selector2 子代关系选择器
匹配 selector1 的下一级满足 selector2 的 一组元素
``` html
...
<style>
section > span { color: red; }
</style>
...
<section>
<span></span> <!-- match -->
<p><span></span></p> <!-- no match -->
</section>
...
```
### selector1 + selector2 相邻兄弟选择器
匹配selector1后同级的紧跟的selector2的一个元素
``` html
...
<style>
h2 + p { color: red; }
</style>
...
<p></p> <!-- no match -->
<h2></h2>
<p></p> <!-- match -->
<p></p> <!-- no match -->
...
```
### selector ~ selector2 一般兄弟选择器
匹配selector1后同级的selector2的一组元素
``` html
...
<style>
h2 ~ p { color: red; }
</style>
...
<p></p> <!-- no match -->
<h2></h2>
<p></p> <!-- match -->
<p></p> <!-- match -->
<span></span>
<p></p> <!-- match -->
...
```
## Group Selectors 组合选择器
### selector1, selector2, ...
匹配用`,` 隔开的所有选择器
``` html
...
<style>
p, span { color: red; }
</style>
...
<section>
<span></span>
<p><span></span></p>
</section>
...
```
## Pseudo-elements 伪元素选择器
### ::first-letter
匹配 element中的首个字符字母、中文字、符号均可
``` html
...
<style>
p::first-letter { color: red; }
</style>
...
<p>One</p> <!-- match: O -->
...
```
### ::first-line
匹配 element中的首行文字
``` html
...
<style>
p::first-line { color: red; }
</style>
...
<p>
One Two <br> <!-- match -->
Three
</p>
...
```
### ::before
`content` 属性一起使用,在匹配的元素内容之前生成的内容
``` html
...
<style>
p::before { content: 'before ' }
</style>
...
<p>
One Two <!-- render: before One Two -->
</p>
...
```
### ::after
`content` 属性一起使用,在匹配的元素内容之后生成的内容
``` html
...
<style>
p::after { content: ' after' }
</style>
...
<p>
One Two <!-- render: One Two after -->
</p>
...
```
## Pseudo-classes 伪类选择器
### :link
匹配一个没有被访问过的链接
``` html
...
<style>
a:link { color: red }
</style>
...
<a href="">link</a>
...
```
### :visited
匹配一个已访问过的链接
``` html
...
<style>
a:visited { color: red }
</style>
...
<a href="">link</a>
...
```
### :active
匹配一个正在被激活的链接
``` html
...
<style>
a:active { color: red }
</style>
...
<a href="">link</a>
...
```
### :hover
匹配一个被光标悬停的链接
``` html
...
<style>
a:hover { color: red }
</style>
...
<a href="">link</a>
...
```
### :focus
匹配一个具有焦点的元素
``` html
...
<style>
input:focus { color: red }
</style>
...
<input type="text">
...
```
### :target
匹配一个已被链接到的元素。
例如通过`<a href="#heading"></a>`链接的head元素
``` html
...
<style>
h2:target { color: red }
</style>
...
<h2 id="heading">heading</h2>
...
```
### :first-child
匹配在同一个父元素内的的第一个子元素
``` html
...
<style>
p:first-child { color: red }
</style>
...
<p>first child</p> <!-- match -->
<p>second child</p>
...
```
### :last-child
匹配在同一个父元素内的的最后一个子元素
``` html
...
<style>
p:last-child { color: red }
</style>
...
<p>first child</p>
<p>last child</p> <!-- match -->
...
```
### :nth-child(n)
匹配在同一个父元素内的从上往下数的第N子个元素
``` html
...
<style>
p:nth-child(2) { color: red }
</style>
...
<p>first child</p>
<p>second child</p> <!-- match -->
...
```
### :nth-last-child(n)
匹配在同一个父元素内的从下往上数的第N个子元素
``` html
...
<style>
p:nth-last-child(2) { color: red }
</style>
...
<p>first child</p> <!-- match -->
<p>second child</p>
...
```
### :first-of-type
匹配在同一个父元素中的同类型的第一个元素
``` html
...
<style>
p:first-of-type { color: red }
</style>
...
<p>first child</p> <!-- match -->
<p>second child</p>
...
```
### :last-of-type
匹配在同一个父元素中的同类型的最后一个元素
``` html
...
<style>
p:last-of-type { color: red }
</style>
...
<p>first child</p> <!-- match -->
<p>second child</p>
...
```
### :nth-of-type(n)
匹配在同一个父元素中的同类型的从上往下数的第N个元素
``` html
...
<style>
p:nth-of-type(2) { color: red }
</style>
...
<p>first child</p>
<p>second child</p> <!-- match -->
...
```
### :nth-last-of-type(n)
匹配在同一个父元素中的同类型的从下往上数的第N个元素
``` html
...
<style>
p:nth-last-of-type(2) { color: red }
</style>
...
<p>first child</p> <!-- match -->
<p>second child</p>
...
```
### :only-child
如果元素是其父元素的唯一子元素,则匹配该元素
``` html
...
<style>
section p:only-child { color: red }
</style>
...
<section>
<p> only child </p>
</section>
...
```
### :only-type
如果元素是其父元素的唯一的同类型的子元素,则匹配该元素
``` html
...
<style>
section p:only-of-type { color: red }
</style>
...
<section>
<p> only </p> <!-- match -->
<span></span>
</section>
...
```
### :lang(lang)
匹配给定语言的元素
``` html
...
<style>
div:lang(fr) { color: red }
</style>
...
<section>
<div lang="fr"><q>This French quote has a <q>nested</q> quote inside.</q></div>
</section>
...
```
### :empty
匹配没有子元素或内容的元素
``` html
...
<style>
div:empty { background-color: red }
</style>
...
<section>
<div></div>
</section>
...
```
### :root
匹配文档的根元素, (即匹配的 `<html>`元素)
### :enabled
匹配未被禁用的表单控件元素
### :disabled
匹配被禁用的表单控件元素
### :checked
匹配选中的单选或复选框类型的输入元素。
### :not(selector)
协商伪类。匹配不匹配选择器的元素。
## 实验中的 Selectors
这些选择器在某些浏览器中尚处于开发中,功能对应的标准文档可能被修改,在未来的版本中可能发生变化,谨慎使用。
### :any-link
匹配有链接锚点的元素,而不管元素是否被访问过。
即会匹配每一个有 `href`属性的`<a>``<area>``<link>`的元素,匹配到所有的`:link``:visited`
``` html
...
<style>
a:any-link {
border: 1px solid blue;
color: orange;
}
</style>
...
<a href="https://example.com">External link</a><br>
<a href="#">Internal target link</a><br>
<a>Placeholder link (won't get styled)</a>
...
```
::: caniuse css-any-link
:::
### :dir(dir)
如果元素的内容的书写方向是 dir , 则匹配该元素
*dir* : ltr | rtl
``` html
...
<style>
:dir(ltr) {
background-color: yellow;
}
:dir(rtl) {
background-color: powderblue;
}
</style>
...
<div dir="rtl">
<span>test1</span>
<div dir="ltr">test2
<div dir="auto">עִבְרִית</div>
</div>
</div>
...
```
::: caniuse css-dir-pseudo
:::
### :has(selector)
如果一个元素A恰好满足包含了selector 匹配的元素则匹配元素A
``` html
...
<style>
a:has(> img) {
background-color: yellow;
}
</style>
...
<a><img src="example.jpg"></a> <!-- match -->
<a></a>
...
```
::: caniuse css-has
:::
### :is() / :any()
匹配一组选择器选中的元素。
优先级是由它的选择器列表中优先级最高的选择器决定。
``` html
...
<style>
:is(header, main, footer) p:hover {
color: red;
cursor: pointer;
}
</style>
...
<!-- 等价于 -->
<style>
header p:hover,
main p:hover,
footer p:hover {
color: red;
cursor: pointer;
}
</style>
...
```
::: caniuse css-matches-pseudo
:::
### :where()
匹配一组选择器选中的元素。
:where() 的优先级总是为 0。
``` html
...
<style>
:where(header, main, footer) p:hover {
color: red;
cursor: pointer;
}
</style>
...
<!-- 等价于, 但优先级不同 -->
<style>
header p:hover,
main p:hover,
footer p:hover {
color: red;
cursor: pointer;
}
</style>
...
```

View File

@ -1,106 +0,0 @@
---
title: <!DOCTYPE> 文档类型声明
createTime: 2018/03/14 01:06:52
permalink: /article/s8udp6vp/
author: pengzhanbo
tags:
- html
---
Web世界中随着历史的发展技术的迭代发展出了许多不同的文档只有了解文档的类型浏览器才能正确的解析渲染文档。
<!-- more -->
HTML也有多个不同的版本只有完全明白页面使用的是哪个确切的HTML版本浏览器才能完全正确的显示出HTML页面。
## 定义
`<!DOCTYPE>` 标签是一种标准通用标记语言的文档类型声明目的是告诉标准通用标记语言解析器它应该使用什么样的文档类型定义DTD来解析文档。
## 作用
声明文档的解析类型 document.compatMode避免浏览器的怪异模式。
__document.compatMode:__
- `BackCompat`: 怪异模式,浏览器使用自己的怪异模式解析渲染页面。
- `CSS1Compat`: 标准模式浏览器使用W3C的标准解析渲染页面。
## 使用
在文档的首行进行声明。必须位于 html标签之前。
`<!DOCTYPE>` 声明不是HTML标签它是指示浏览器关于页面使用哪个HTML版本的指令。
> 如果页面没有 DOCTYPE 声明,那么默认是 怪异模式为了确保浏览器按预期渲染页面必须进行DOCTYPE声明。
### 常用的DOCTYPE声明
一般情况下,默认使用以下声明即可。
``` html
<!DOCTYPE html>
<html>
</html>
```
## 一般DOCTYPE声明列表
### html5
``` html
<!DOCTYPE html>
```
### HTML 4.01 Strict
该 DTD 包含所有 HTML 元素和属性,但不包括展示性的和弃用的元素(比如 font。不允许框架集Framesets
``` html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
```
### HTML 4.01 Transitional
该 DTD 包含所有 HTML 元素和属性,包括展示性的和弃用的元素(比如 font。不允许框架集Framesets
``` html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
```
### HTML 4.01 Frameset
该 DTD 等同于 HTML 4.01 Transitional但允许框架集内容。
``` html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN"
"http://www.w3.org/TR/html4/frameset.dtd">
```
### XHTML 1.0 Strict
该 DTD 包含所有 HTML 元素和属性,但不包括展示性的和弃用的元素(比如 font。不允许框架集Framesets。必须以格式正确的 XML 来编写标记。
``` html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
```
### XHTML 1.0 Transitional
该 DTD 包含所有 HTML 元素和属性,包括展示性的和弃用的元素(比如 font。不允许框架集Framesets。必须以格式正确的 XML 来编写标记。
```html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "
http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
```
### XHTML 1.0 Frameset
该 DTD 等同于 XHTML 1.0 Transitional但允许框架集内容。
```html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">
```
### XHTML 1.1
该 DTD 等同于 XHTML 1.0 Strict但允许添加模型例如提供对东亚语系的 ruby 支持)。
``` html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
```

View File

@ -1,273 +0,0 @@
---
title: HTML5新特性
createTime: 2018/02/17 12:49:58
permalink: /article/8rv45yuy/
author: pengzhanbo
tags:
- html
---
## 语义标签
`<header>` `<footer>` `<nav>` `<section>` `<article>` `<aside>` `<details>` `<summary>` `<dialog>` ` <figure>` `<main>` `<mark>` `<time>` `<hgroup>`
## 增强型表单
### 新增表单元素
`<detailist>` 数据列表为input提供输入建议列表
`<progress>`:进度条,展示连接/下载进度
`<meter>`:刻度尺/度量衡,描述数据所处的阶段,红色(危险)=>黄色(警告)=>绿色(优秀)
`<output>`:输出内容,语义上表示此处的数据是经过计算而输出得到的
其他
### 新增表单属性
placehoder 输入框默认提示文字
required 要求输入的内容是否可为空
pattern 描述一个正则表达式验证输入的值
min/max 设置元素最小/最大值
step 为输入域规定合法的数字间隔
height/wdith 用于image类型`<input>`标签图像高度/宽度
autofocus 规定在页面加载时,域自动获得焦点
multiple 规定`<input>`元素中可选择多个值
### 新增 input type 类型
color 颜色选取
date 日期选择
datetime 日期选择UTC时间
datetime-local 日期选择(无时区)
month 月份选择
week 周和年 选择
time 选择时间
email 包含 email的地址输入域
number: 数值选择
url url输入域
tel 电话号码和字段
search 搜索域
range 数字范围输入域
## 视频和音频
`<audio>` 音频元素
```html
<audio controls>
<source src="horse.ogg" type="audio/ogg">
<source src="horse.mp3" type="audio/mpeg">
您的浏览器不支持 audio 元素
</audio>
```
`<video>` 视频元素
```html
<video width="320" height="240" controls>
<source src="movie.mp4" type="video/mp4">
<source src="movie.ogg" type="video/ogg">
您的浏览器不支持Video标签。
</video>
```
## Canvas绘图
`<canvas>` 是 HTML5 新增的,一个可以使用脚本(通常为 JavaScript) 在其中绘制图像的 HTML 元素。它可以用来制作照片集或者制作简单(也不是那么简单)的动画,甚至可以进行实时视频处理和渲染。
[](https://www.runoob.com/w3cnote/html5-canvas-intro.html)
## 地理位置
使用getCurrentPosition()方法来获取用户的位置。以实现“LBS服务”
```jsx
window.navigator.geolocation : {
  watchPosition(){},
  clearWatch(){},
  getCurrentPosition(function(pos){
    // '定位成功'
    // 定位时间pos.timestamp
    // 维度pos.coords.latitude
    // 经度pos.coords.longitude
    // 海拔pos.coords.altitude
    // 速度pos.coods.speed
  }, function(err){
    // '定位失败'
  }){},
}
```
## 拖放API
### 拖动的源对象(source)可能触发的事件:
**dragstart**:拖动开始
**drag**:拖动中
**dragend**:拖动结束
### 拖动的目标对象(target)可能触发的事件:
**dragenter**:拖动进入
**dragover**:拖动悬停
**drop**:松手释放
**dragleave**:拖动离开
拖放API事件句柄中所有的事件对象都有一个dataTransfer属性数据运输对象用于在源对象和目标对象间传递数据。
**源对象**event.dataTransfer.setData(key, value)
**目标对象**var value = event.dataTransfer.getData(key)
## WebWorker
[使用 Web Workers - Web API 接口参考 | MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API/Using_web_workers)
### 背景
Chrome浏览器中发起资源请求的有6个线程但是只有1个线程负责渲染页面——称为UI主线程——浏览器中所有的代码只能由一个线程来执行。
### 问题
若浏览器加载了一个很耗时的JS文件(可能影响DOM树结构),浏览器必须等待该文件执行完成才会继续执行后续的代码(HTML/CSS/JS等)——如果一个JS文件要执行10s(可能有很深的循环/递归等科学计算/解密)会发生什么——执行耗时JS任务过程中会暂停页面中一切内容的渲染以及事件的处理。
### 作用
一个执行指定任务的独立线程且该线程可以与UI主线程进行消息数据传递。
使用方式:
```jsx
// 主线程
var worker = new Worker('xx.js')
worker.postMessage('message') // 发送消息到worker线程
worker.onmessage = function (e) {
console.log(e.data) // 来自worker线程的信息
}
// worker线程
onmessage = function (e) {
console.log(e.data) // 接收主线程的消息
postMessage('message') // 发送消息到主线程
}
```
### 共享 worker
一个共享worker可以被多个脚本使用——即使这些脚本正在被不同的window、iframe或者worker访问。
> 如果共享worker可以被多个浏览上下文调用所有这些浏览上下文必须属于同源相同的协议主机和端口号
>
```jsx
var myWorker = new SharedWorker('worker.js');
// 主线程中调用
myWorker.port.start()
myWorker.port.postMessage('message');
myWorker.port.onmessage = function(e) {
console.log('Message received from worker');
}
// worker 线程调用
port.start();
// worker 需要在 onconnect事件处理函数来执行代码
onconnect = function(e) {
var port = e.ports[0];
port.onmessage = function(e) {
var workerResult = 'Result: ' + (e.data);
port.postMessage(workerResult);
}
}
```
## WebStorage
### localStorage
本地跨会话级,持久化存储
### sessionStorage
会话级存储
## WebSocket
在用户的浏览器和服务器之间打开交互式通信会话。
```jsx
const ws = new WebSocket('wx://xx')
ws.onopen = function () {}
ws.onmessage = function (e) {
console.log(e.data)
}
```
## History API
对history栈中内容进行操作。
### pushState(stateObj, title, url)
```jsx
history.pushState({}, 'foo', 'foo.html')
```
添加历史记录条目
### replaceState(stateObj, title, url)
```jsx
history.replaceState({}, 'bar', 'bar.html')
```
修改历史记录条目,浏览器不会检查替换的路径是否存在。
### popState 事件
每当活动的历史记录项发生变化时, popstate 事件都会被传递给window对象。如果当前活动的历史记录项是被 pushState 创建的,或者是由 replaceState 改变的,那么 popstate 事件的状态属性 state 会包含一个当前历史记录状态对象的拷贝。
### 获取当前状态
页面加载时或许会有个非null的状态对象。这是有可能发生的举个例子假如页面通过pushState() 或 replaceState() 方法设置了状态对象而后用户重启了浏览器。那么当页面重新加载时页面会接收一个onload事件但没有 popstate 事件。然而假如你读取了history.state属性你将会得到如同popstate 被触发时能得到的状态对象。
```jsx
// 尝试通过 pushState 创建历史条目,然后再刷新页面查看state状态对象变化;
window.addEventListener('load',() => {
let currentState = history.state;
console.log('currentState',currentState);
})
```
[History API - Web API 接口参考 | MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/History_API)

View File

@ -1,131 +0,0 @@
---
title: WebComponent——template
lang: zh-CN
tags:
- WebComponent
- javascript
createTime: 2018/8/2 11:15:27
permalink: /article/5fmy4kla/
author: pengzhanbo
---
在web开发领域中模板并不少见。从服务器端的模板语言`Django``jsp`等,应用十分广泛,存在了很长时间。又如前端,早期例如`art(artTemplate)`以及近年来大多数的MV*框架涌现,绝大多数在展现层使用了同样的渲染机制:模板。
<!-- more -->
> __定义__
>模板,一个拥有预制格式的文档或者文件,可作为特定应用的出发点,这样就避免在每次使用格式的时候都重复创建。
从模板的定义中我们可以发现“避免在每次使用格式的时候重复创建”从这句话来看模板可以让我们避免重复的工作。那么web平台有没有提供原生支持呢
答案是,有,在 [WhatWG HTML 模板规范](https://html.spec.whatwg.org/multipage/scripting.html#the-template-element)中,它定义了一个新的`<template>` 元素用于描述一个标准的以DOM为基础的方案来实现客户端模板。该模板允许你定义一段可以被转为 HTML 的标记,在页面加载时不生效,但可以在后续进行动态实例化。
### 声明
跟普通的html标签一样`template`标签包含的内容,即是声明的模板内容。
``` html
<template>
<img src="" />
<p>content</p>
</template>
```
“模板内容”本质上,是 __一大块的惰性可复制DOM__。在这个例子中,标签内的元素并不会被渲染,图片资源也不会发出请求。模板可以理解为单个零件,在整个应用的生命周期中,你都可以使用、以及重用它。
### 特性
使用`<template>`标签包裹我们的内容,可以为我们提供一下几个重要的特性。
1. __它的内容在激活前都是惰性的。__ template标签默认是隐藏的它的内容也是不可见的同时也不会被渲染。
2. __处于模板中的内容不会产生副作用。__ 放在模板中的脚本、音频、视频、图片资源不会被加载,不会被播放,直到模板中的内容被使用。
3. __内容不在文档中。__ 在主页面使用`document.getElementById()`,不会返回模板子节点。
4. __模板能够放置在任何位置。__ 你可以把`<template>` 放置在`<head>``<body>``<frameset>`并且任何能够出现在以上元素的内容都可以放置在模板中。__“任何位置”__ 意味着`<template>`标签可以出现在HTML解析器不允许出现的位置 _(必须是在`<html>`标签内)_,几乎可以作为任何元素的子节点。它也可以作为`<table>``<select>`的子节点。当然,如果写在声明`type="text/javascript"``<script>`标签中,绝对报错,原因我就不说了。(同时实测发现,如果`<template>`标签放在`<head>``<body>`同级,放在`<body>`前面,都会被解析到`<head>`标签内,放在`<body>`后,会被解析到`<body>`内)。
``` html
<table>
<tr>
<templete>
<td>content</td>
</templete>
</tr>
</table>
```
### 使用模板
想要使用模板,首先需要激活模板,否则它的内容将无法被渲染。模板对象包含了一个`content`属性,该属性是只读属性,关联一个包含模板内容的`DocumentFragment`
我们可以使用`document.importNode()`对模板的`.content`进行深拷贝。
``` html
<template id="template1">
<img src="" />
<p>content</p>
</template>
```
``` javascript
var tmp = document.querySelector('#template1');
// 可以在获取模板的时候,对内容进行填充
tmp.content.querySelector('img').scr = 'logo.png';
var clone = document.importNode(tmp.content, true);
document.body.appendChild(clone);
```
模板中的资源,比如图片资源,只有被激活后,才会发出请求。
### 浏览器支持
想要检测浏览器是否支持该标签需要创建一个template元素并检查它是否拥有`.content`属性。
``` javascript
function supportTemplate() {
return 'content' in document.createElement('template');
}
if (supportsTemplate()) {
// 浏览器支持 template 元素
} else {
// 浏览器不支持template元素
}
```
从目前来看IE13+开始支持低于此版本的IE均无法使用如果有项目只需要考虑 webkit内核的浏览器template标签还是可以一用。
::: caniuse mdn-html__elements__template
:::
如果浏览器不支持template标签那么就会认为是一个普通的自定义元素内部的标签会被作为一般的标签被渲染。
### 模板标准之路
HTML 模板标准化进程耗时十分长久。从过去到现在,出现了很多各种各样的方法去创建可重用的模板。
__方法一使用隐藏的DOM元素将模板内容放在某个标签内使用`display:none`隐藏元素。__
``` html
<div style="display:none">
<img src="" />
</div>
```
使用这种方式,有利有弊:
1. √ 使用DOM浏览器能够很好的处理DOM结构我们可以方便的复制、使用DOM。
2. √ 没有内容渲染,`display: none` 阻止了内容渲染。
3. × 非惰性, 图片资源依然会发出请求。
4. x 难以设置样式和主题需要为所有CSS增加规则。
__方法二使用textarea 标签,并使用`display:none`隐藏元素。__
``` html
<textarea style="display:none">
<img src="" />
</textarea>
```
这种方法是对方法一的一种改进,但也有新的利弊:
1. √ 没有内容渲染,`display: none` 阻止了内容渲染。
2. √ 惰性,由于模板内容是字符串,图片资源不会发出请求。
3. x 模板内容是字符串需要进一步将其转为DOM。
__方法三 重载脚本__
``` html
<script type="text/x-handlebars-template">
<img src="" />
</script>
```
利弊:
1. √ 没有内容渲染script 标签默认`display:none`
2. √ 惰性,脚本类型不为 `text/javascript`浏览器不会认为是脚本不会将其作为JS解析。
3. x 安全问题,由于使用 `innerHTML`获取内容,对用户提供的字符串进行运行时解析,很容易倒是 XSS漏洞。
### 总结
模板标准化使得我们在做web开发整个过程更加健全更容易维护。

View File

@ -1,231 +0,0 @@
---
title: WebComponent——custom elements
tags:
- WebComponent
- javascript
createTime: 2018/08/01 11:15:27
permalink: /article/m63fd7lf/
author: pengzhanbo
---
在我们的web应用开发中HTML标签为我们提供了基础的应用和交互我们使用HTML标签构建了各种各样丰富的web应用。
然而在我们开发web应用的过程中html标签提供的语义化并不能完全满足我们的场景。虽然在HTML5标准中也增加了不少包括`<header>``<section>``<article>``<nav>``<container>``<footer>`等语义化标签,但它们主要是为内容或布局添加的通用语义化标签,在实际的场景中,我们还需要使用 `class` 等一些属性或者辅助说明,声明该标签的具体语义。
<!-- more -->
``` html
<div class="login-wrapper"></div>
```
如果可以这么做呢:
``` html
<login></login>
```
使用更加语义化的标签,满足我们各种场景,甚至是扩展已有标签的特性。那么我们该怎么做呢?
接下来是我们的主角: __[自定义元素custom Elements](http://w3c.github.io/webcomponents/spec/custom/)__
### 自定义元素
> 自定义元素能够帮助web开发者创建拥有自身特性的自定义标签。
### 创建自定义元素
_创建自定义元素有两种方式这里只讨论 __DOM LEVEL 3__ 提供的 `customElements`,在 __DOM LEVEL 2__ 中的 `document.registerElement` 将作为补充内容在本文最后补充。_
[Custom Element API 规范](http://w3c.github.io/webcomponents/spec/custom/) 定义了`customElements`作为统一的对象管理自定义元素并对ES6 class提供了更完善的支持。
>规范还定义了 `CustomElementRegistry`, 并且 `customElements instanceof CustomElementRegistry`
我们可以通过 `customElements.define()` 方法来注册一个custom element该方法接受以下参数
`customElements.define(tarName, class[, option])`
- `tarName`: `DOMString`,用于表示所创建的元素名称。名称必须是小写字母开头,且必须包含至少一个`-`,任何不含`-`的自定义标签都会导致错误。例如`my-tag`,`my-list-item`为合法标签,`my_tag`,`myTag`都是非法的自定义标签名称;
- `class`: 类对象,用于定义元素行为.
- `option`: 包含 `extends` 属性的配置对象,可以指定所创建的元素继承自那个内置元素,可以继承任何内置元素;
`customElements`的类对象可以通过 ES 2015的类语法定义
``` javascript
class MyTag extends HTMLElement {
constructor() {
super();
}
}
customElements.define("my-tag", MyTag);
```
### 使用自定义元素的生命周期回调函数
`customElements`的构造函数中,我们可以指定多个不同的回调函数,他们会在不同的声明周期被触发。
- `connectedCallback`: 元素首次插入到文档DOM时回调
- `discannectedCallback`: 元素从文档DOM中删除时回调
- `attributeChangedCallback` 元素增加、删除、修改自身属性时回调;
- `adoptedCallback`:元素被移动到新的文档时回调;
``` javascript
class MyCustom extends HTMLElement {
// 自定义元素开始提升时调用
// 元素提升并不说明元素已插入到文档中
// 在此阶段尽量避免进行DOM操作
constructor() {
super();
}
// 元素插入到文档时回调
connectedCallback() {
// do something...
}
// 元素从文档中删除时回调
discannectedCallback() {
// do something...
}
/*
* 元素属性变化回调
* @param name {string} 变化的属性名
* @param oldValue {any} 变化前的值
* @param newVlalue {any} 变化后的值
*/
attributeChangedCallback(name, oldValue, newValue) {
// do something...
}
// 元素被移动到新的文档中时调用
// When it is adopted into a new document, its adoptedCallback is run.
// 具体场景示例通过document.adoptNode方法修改元素ownerDocument属性时可以触发
adoptedCallback() {
// do something...
}
}
```
如果需要在元素属性发生变化后触发 `attributeChangedCallback`,就必须监听这些属性。 我们可以通过定义静态属性`observedAttributed`的 get函数来添加需要监听的属性
``` javascript
static get observedAttributed() {
return ['name'];
}
```
### 使用自定义元素
我们可以在文档的任何地方使用`customElements.define`注册的自定义元素,即使是在自定义元素注册之前。
``` html
<my-tag></my-tag>
```
或者:
``` js
class MyTag extends HTMLElement {
constructor() {
super();
}
}
customElements.define("my-tag", MyTag);
// 方式一:
var tag = document.createElement('my-tag');
document.appendChild(tag);
// 方式二:
var tag = new MyTag();
document.appendChild(tag);
```
### 元素提升
浏览器是如何解析非标准的标签的?为什么对非标准的标签,浏览器不会报错?
> HTML规范
> 非规范定义的元素必须使用 _HTMLUnknownElement_ 接口。
我们在页面中声明一个 `<myTag>`标签,由于它是非标准标签,所以会继承 `HTMLUnknownElement`
对于自定义元素,情况有所不同。 拥有合法元素名称的自定义元素继承自`HTMLElement`
对于不支持自定义元素的浏览器,拥有合法元素名称的标签,仍然继承`HTMLUnknownElement`
### 扩展内置元素特性
在创建自定义元素时,置顶所需的扩展的元素,使用时,在内置元素上声明`is`属性指定自定义元素名称:
``` js
class CustomButton extends HTMLButtonElement {
constructor() {
super();
}
}
customElements.define("custom-button", CustomButton, {
extends: 'button'
});
```
``` html
<button is="custom-button"></button>
```
### 自定义元素样式
自定义元素和内置元素一样可以使用CSS各类选择器定义样式。
自定义元素规范还提出了一个新的CSS伪类`:unresolved`。在浏览器调用你的`createdCallback()` 之前,这个伪类可以匹配到未完成元素提升的自定义元素。
``` css
custom-button{
opacity: 1;
transition: opacity 300ms;
}
custom-button:unresolved{
opacity: 0
}
```
> :unresolved 不能用于继承自HTMLUnkownElement的元素。
### 浏览器支持
`Chrome``Opera`默认支持custom elements。`Firefox`计划在60/61的版本中默认支持自定义元素。`Safair`目前不支持自定义元素对内置元素的扩展。`Edge`在实现中。
### 补充内容:`document.registerElement`
使用`document.registerElement()` 创建自定义元素
``` javascript
var MyTag = document.registerElement('my-tag');
```
添加自定义元素特性:
``` javascript
var proto = Object.create(HTMLElement.prototype);
proto.hello = 'hello';
proto.sayHello = function () {
alert(this.hello);
};
var MyTag = document.registerElement('my-tag', {
prototype: proto
});
```
扩展原生元素特性
`document.registerElement()` 的第二个参数还允许我们为扩展原生素的特性。
``` javascript
var MyButton = document.registerElement('my-button', {
extend: 'button',
prototpye: Object.create(HTMLButtonElement.prototype)
});
```
``` html
<button is="my-button"><button>
```
生命周期以及回调方法
1. createdCallback(): 元素创建后回调。
2. attachCallback(): 元素附加到文档后调用。
3. detachCallback(): 元素从文档移除后调用。
4. attributeChangedCallback(): 元素任意属性变化后调用。
``` javascript
var myTagProto = Object.create(HTMLElement.prototype);
myTagProto.createdCallback = function() {
// 元素创建后回调。
this.textContent = '我被创建了';
};
var MyTag = document.registerElement('my-tag', {
prototype: myTagProto
});
```
### 结语
自定义元素作为 `webComponent` 规范中的一部分为web应用开发提供了更多的可能性配合`webComponent` 规范的其他内容可以为web开发者提供更强大的能力。

View File

@ -1,239 +0,0 @@
---
title: javascript模块化 发展历程
createTime: 2022/04/10 03:00:41
author: pengzhanbo
permalink: /article/javascript-modules/
---
javascript模块化的发展距今已有10个年头左右。
## 无模块化
在早期javascript作为一门脚本语言仅为协助表单校验等界面辅助增强那时候的前端也比较简单 javascript不需要模块化。
## 命名空间
后来随着 javascript 需要承担更多的功能,代码量开始上升,为了避免全局命名冲突等问题,提出了使用命名空间的方案,将符合某种规则或者约定的代码,放到同一个命名空间下。 这算是 javascript模块化最早期的雏形。
``` js
YAHOO.util.Event.stopPropagation(e);
```
## 基本的模块化
在这个时期,出现了比较清晰的模块定义,通过闭包来做模块运行空间
``` js
// 定义模块
YUI.add('hello', function(Y) {
Y.sayHello = function() {
Y.DOM.set(el, 'innerHTML', 'hello!');
}
}, '1.0.0',
requires: ['dom']);
...
// 使用模块
YUI().use('hello', function(Y) {
Y.sayHello('entry'); // <div id="entry">hello!</div>
})
```
## CommonJs
CommonJs 其实是一个项目,其目标是为 JavaScript 在网页浏览器之外创建模块约定, 在当年 javascript 的模块化思想还在官方的讨论中, 缺乏普遍可接受形式的javascript脚本模块单元。
CommonJs规范和当时出现的NodeJs相得益彰共同走入了开发者的实现。
但 CommonJs 其实是面向网页浏览器之外的如NodeJs即面向服务端的模块化规范并不适用于浏览器端。
### CommonJs 规范简介
在CommonJs 规范中, 每个文件都是一个模块,有自己的作用域,在文件中定义的变量、函数、类等,都是私有的,对其他文件不可见。
在每个模块中,有两个内部变量可以使用, `require``module`
- `require` 用于加载某个模块。
- `module` 表示当前模块,是一个对象。这个对象中保存了当前模块的信息。`exports``module` 上的一个属性,保存了当前模块要导出的接口或者变量,使用 `require` 加载的某个模块获取到的值就是那个模块使用 `exports` 导出的值。
::: code-tabs
@tab a.js
``` js
var name = 'Mark';
var age = 18;
module.exports.name = name;
module.exports.getAge = function () {
return age;
}
```
@tab:active b.js
``` js
var moduleA = require('./a.js')
console.log(moduleA.a); // Mark
// 使用了未导出的变量,获取不到值
console.log(moduleA.age) // undefined
console.log(moduleA.getAge()); // 18
```
:::
在NodeJs环境中CommonJs的模块由于在服务器环境下可以从本地进行加载即 同步加载。
## AMD、CMD
::: note 注释
在我的印象中, CommonJs规范 和 AMD规范 出现的时间点 相差不远。
*AMD 早于 CommonJs*
按我个人理解CMD 在当年算是从 AMD 衍生出来的一个方案。
:::
::: warning 注意
CommonJs 和 CMD 是两种方案!不是一样的!
:::
### AMD规范
AMD规范即 异步模块定义([Asynchronous Module Definition](https://github.com/amdjs/amdjs-api/wiki/AMD))。
AMD 采用 __异步加载模块__ 的方式。
AMD规范仅定义了一个 `define` 函数,它是一个全局变量:
```
define(id?, dependencies?, factory);
```
- `id` 描述的是当前模块的标识符;
- `dependencies` 则是当前模块的依赖数组, 它们会在 factory工厂方法被调用前被加载并执行
并且执行的结果必须以依赖数组定义的顺序,依此顺序作为参数传入 factory工厂方法。
- `factory`为模块初始化要执行的函数或者对象。如果函数返回一个值,则该值应该设置为该模块的输出值。
### CMD规范
CMD规范即 公共模块定义([Common Module Definition](https://github.com/cmdjs/specification/blob/master/draft/module.md))
CMD规范 定义了 一个 `define` 函数,它是一个全局变量:
```
define(id?, dependencies?, factory);
```
- `id` 描述的是当前模块的标识符;
- `dependencies` 是当前模块的依赖数组, 他们会在 factory 工厂方法被调用前完成加载,但并不立即执行。
- `factory`为模块初始化要执行的函数或者对象。
- 如果是一个函数,则函数接受三个参数:
``` js
define(function (require, exports, module))
```
`require` 用于同步加载并执行已经定义好的其他模块;获取模块的输出值,
`exports``module.exports`的别名,用于导出当前模块的输出值;`module`存储了当前模块的信息。
- 如果是一个对象,则直接作为当前模块的输出值。
::: tip 两者的差异
AMD规范 和 CMD规范 从规范定义上来看,主要的差异为:
- AMD 的模块在加载后是立即执行的,并且会按照依赖顺序依次传入 factory
而 CMD的模块在加载后并不立即执行而是在 factory方法中通过 `require` 方法调用执行模块获取结果;
:::
### 规范的实现
- AMD流行的实现库是 [require.js](https://github.com/requirejs/requirejs);
- CMD流行的实现库是 [sea.js](https://github.com/seajs/seajs);
::: warning 提示
由于在当下已经越来越少会去选择使用 `require.js` 以及 `sea.js` 这里就不多对这两个库做介绍说明。
:::
## NodeJs前端工具链
得益于 NodeJs 的能力,开源社区在模块化方面又再次向前继续迈进。 特别是在推出了 `NPM` 包管理工具后,前端的工具、模块化出现了井喷式发展。
### grunt gulp
既然 CommonJs 不适用于 浏览器端的一个主要原因是同步加载和异步加载之间的问题,那么借助于 `grunt``gulp` 提供的前端工具,在开发时,还是以文件一模块,然后构建时,将模块文件打包在一起,那么由于都是在同一个文件中,则模块之间的加载则可以是同步的。
在这个时期,`grunt``gulp` 并没有提供直接的模块化打包能力,但是在其基础上,通过插件实现了文件合并,从而能够在开发时,以 某种模块规范进行项目架构和管理,再进行打包构建。
### webpack NPM
真正让 前端模块化得到质的飞跃的,是 NPM的推出内置到了 NodeJS 中。
而 webpack 的出现,这块 真正意义上的 模块打包工具,配合 NPM 让模块化越来越得以更方便的运用于应用开发中。
webpack 作为一个 模块打包器, 在内部根据 CommonJs规范实现了 模块加载器使得应用于浏览器端的javascript代码也能够像 Node端的 javascript代码拥有类似甚至相同的文件组织结构。
实现了一文件一模块,模块之间通过 `require` 函数进行 访问。
而 NPM的推出与流行在前端引入了 `package` 包的概念,模块以包的形式进行管理, 让越来越多的开发者,能够共享各自开发的模块,开发者可以通过 NPM 安装其他开发者已开发好的模块,然后通过 `webpack` 实现开发时加载这些模块。
webpack 内部实现了 不同的 模块化规范,包括 匿名函数闭包`iife`, `AMD`, `CMD`,`CommonJs`等。
`webpack` 不仅将 javascript 作为模块,而是将一切资源都作为模块进行处理。
### 其他的模块打包工具
- [rollup](https://github.com/rollup/rollup) 轻量且快速的模块打包工具
- [parcel](https://github.com/parcel-bundler/parcel) 零配置的开箱即用的模块打包工具
- [vite](https://github.com/vitejs/vite) 基于rollup的前端工具
- more...
### 其他包管理工具
- [yarn](https://classic.yarnpkg.com/lang/en/)
- [pnpm](https://pnpm.io/)
- more...
::: info 说明
`npm` 对比,都是社区对于 包管理 的不同理念、不同实践 下所产生的工具。
三者互相发展,并都有各自的特色。
:::
## ES Modules
[ES Modules](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Modules)
随着 javascript的发展ECMAScript将模块加载添加到了标准之中浏览器也开始支持 模块加载。
使用 Javascript 模块依赖于 `import``export` 进行导入和导出。
`html` 导入 javascript模块脚本是需要在 `<script>` 标签中添加 `type="module"` 的属性声明
``` html
<script type="module" src="/moduleA.js"></script>
```
::: code-tabs
@tab moduleA.js
``` js
import { name, getAge } from './moduleB.js';
console.log(name);
console.log(getAge());
```
@tab moduleB.js
``` js
export const name = 'Mark';
const age = 18;
export function getAge() {
return age;
}
```
:::
## Deno模块加载
Deno与 Node在模块加载上最大的差别 就是 放弃了 项目中的`node_modules` 作为第三方包的存放目录,也抛弃了 类似于 NPM 的中心化管理的 模块管理工具。
Deno 推荐使用的是 去中心化的模块加载管理,支持直接从远程的任意站点加载提供的模块。
如从 官方的 [deno.lang](https://deno.land/),或者从 [unpkg.com](https://unpkg.com/) 加载第三方模块。
::: info 说明
这种去中心化模块管理的模块加载方案,相对来说会比较依赖于网络环境,虽然远程的模块首次加载后也会被缓存,但进行生产部署时,往往生产服务器跟公网是隔离的,在这种情况下,就需要自建一个内部服务器作为代理,托管第三方的模块包。
:::

View File

@ -1,217 +0,0 @@
---
title: meta 标签说明
createTime: 2018/03/15 01:21:48
permalink: /article/bp1nxjs6/
author: pengzhanbo
tags:
- html
---
<meta> 标签提供关于 HTML 文档的元数据。它不会显示在页面上,但是对于机器是可读的。可用于浏览器(如何显示内容或重新加载页面),搜索引擎(关键词),或其他 web 服务。
<!-- more -->
## 定义
提供有关页面的元信息meta-information比如针对搜索引擎和更新频度的描述和关键词。
## 用法
标签位于文档的头部,不包含任何内容。<meta> 标签的属性定义了与文档相关联的名称/值对。
## 属性
| 属性 | 是否可选 | 描述 |
| :----: | :----: | :---- |
| content | 必选 | 定义与 http-equiv 或 name 属性相关的元信息。 |
| http-equiv | 可选 | 把 content 属性关联到 HTTP 头部。 |
| name | 可选 | 把 content 属性关联到一个名称。 |
| charset | 可选 | 定义编码格式 |
## 常用meta标签说明
### charset
charset是声明文档使用的字符编码主要用于解决编码问题导致的乱码。 charset一定要写在第一行。
两种charset的写法
```html
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
```
## viewport
viewport主要是影响移动端页面布局
```html
<meta name="viewport" content="width=device-width, initial-scale=1.0">
```
**content 参数:**
1. **width** viewport 宽度(数值/device-width)
2. **height** viewport 高度(数值/device-height)
3. **initial-scale** 初始缩放比例
4. **maximum-scale** 最大缩放比例
5. **minimum-scale** 最小缩放比例
6. **user-scalable** 是否允许用户缩放(yes/no)
### SEO优化相关
```html
<!-- 页面标题<title>标签(head 头部必须) -->
<title>your title</title>
<!-- 页面关键词 keywords -->
<meta name="keywords" content="your keywords">
<!-- 页面描述内容 description -->
<meta name="description" content="your description">
<!-- 定义网页作者 author -->
<meta name="author" content="author,email address">
<!-- 定义网页搜索引擎索引方式robotterms 是一组使用英文逗号「,」分割的值,
通常有如下几种取值nonenoindexnofollowallindex和follow。 -->
<meta name="robots" content="index,follow">
```
**robots具体参数如下**
1. none : 搜索引擎将忽略此网页等价于noindexnofollow。
2. noindex : 搜索引擎不索引此网页。
3. nofollow: 搜索引擎不继续通过此网页的链接索引搜索其它的网页。
4. all : 搜索引擎将索引此网页与继续通过此网页的链接索引等价于indexfollow。
5. index : 搜索引擎索引此网页。
6. follow : 搜索引擎继续通过此网页的链接索引搜索其它的网页。
### 移动端常用的meta
```html
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<!-- 删除苹果默认的工具栏和菜单栏 -->
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<!-- 设置苹果工具栏颜色 -->
<meta name="format-detection" content="telphone=no, email=no" />
<!-- 忽略页面中的数字识别为电话忽略email识别 -->
<!-- 启用360浏览器的极速模式(webkit) -->
<meta name="renderer" content="webkit">
<!-- 避免IE使用兼容模式 -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- 针对手持设备优化主要是针对一些老的不识别viewport的浏览器比如黑莓 -->
<meta name="HandheldFriendly" content="true">
<!-- 微软的老式浏览器 -->
<meta name="MobileOptimized" content="320">
<!-- uc强制竖屏 -->
<meta name="screen-orientation" content="portrait">
<!-- QQ强制竖屏 -->
<meta name="x5-orientation" content="portrait">
<!-- UC强制全屏 -->
<meta name="full-screen" content="yes">
<!-- QQ强制全屏 -->
<meta name="x5-fullscreen" content="true">
<!-- UC应用模式 -->
<meta name="browsermode" content="application">
<!-- QQ应用模式 -->
<meta name="x5-page-mode" content="app">
<!-- windows phone 点击无高光 -->
<meta name="msapplication-tap-highlight" content="no">
```
### 百度禁止转码
百度会自动对网页进行转码,这个标签是禁止百度的自动转码
```html
<meta http-equiv="Cache-Control" content="no-siteapp" />
```
### Microsoft Internet Explorer
```html
<!-- 优先使用最新的ie版本 -->
<meta http-equiv="x-ua-compatible" content="ie=edge">
<!-- 是否开启cleartype显示效果 -->
<meta http-equiv="cleartype" content="on">
<meta name="skype_toolbar" content="skype_toolbar_parser_compatible">
<!-- Pinned Site -->
<!-- IE 10 / Windows 8 -->
<meta name="msapplication-TileImage" content="pinned-tile-144.png">
<meta name="msapplication-TileColor" content="#009900">
<!-- IE 11 / Windows 9.1 -->
<meta name="msapplication-config" content="ieconfig.xml">
```
### Google Chrome
```html
<!-- 优先使用最新的chrome版本 -->
<meta http-equiv="X-UA-Compatible" content="chrome=1" />
<!-- 禁止自动翻译 -->
<meta name="google" value="notranslate">
```
### 360浏览器
```html
<!-- 选择使用的浏览器解析内核 -->
<meta name="renderer" content="webkit|ie-comp|ie-stand">
```
### UC手机浏览器
```html
<!-- 将屏幕锁定在特定的方向 -->
<meta name="screen-orientation" content="landscape/portrait">
<!-- 全屏显示页面 -->
<meta name="full-screen" content="yes">
<!-- 强制图片显示,即使是"text mode" -->
<meta name="imagemode" content="force">
<!-- 应用模式,默认将全屏,禁止长按菜单,禁止手势,标准排版,强制图片显示。 -->
<meta name="browsermode" content="application">
<!-- 禁止夜间模式显示 -->
<meta name="nightmode" content="disable">
<!-- 使用适屏模式显示 -->
<meta name="layoutmode" content="fitscreen">
<!-- 当页面有太多文字时禁止缩放 -->
<meta name="wap-font-scale" content="no">
```
### QQ手机浏览器
```html
<!-- 锁定屏幕在特定方向 -->
<meta name="x5-orientation" content="landscape/portrait">
<!-- 全屏显示 -->
<meta name="x5-fullscreen" content="true">
<!-- 页面将以应用模式显示 -->
<meta name="x5-page-mode" content="app">
```
### Apple iOS
```html
<!-- Smart App Banner -->
<meta name="apple-itunes-app"
content="app-id=APP_ID,affiliate-data=AFFILIATE_ID,app-argument=SOME_TEXT">
<!-- 禁止自动探测并格式化手机号码 -->
<meta name="format-detection" content="telephone=no">
<!-- Add to Home Screen添加到主屏 -->
<!-- 是否启用 WebApp 全屏模式 -->
<meta name="apple-mobile-web-app-capable" content="yes">
<!-- 设置状态栏的背景颜色,只有在 “apple-mobile-web-app-capable” content=”yes” 时生效 -->
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<!-- 添加到主屏后的标题 -->
<meta name="apple-mobile-web-app-title" content="App Title">
```
### Google Android
```html
<meta name="theme-color" content="#E64545">
<!-- 添加到主屏 -->
<meta name="mobile-web-app-capable" content="yes">
```

View File

@ -1,182 +0,0 @@
---
title: 继承与原型链
createTime: 2018/07/06 09:40:54
permalink: /article/extends-prototype/
author: pengzhanbo
tags:
- javascript
---
当谈到继承时javascript只有一种结构对象。
每个实例对象object都有一个私有属性`__proto__`指向它的构造函数的原型对象 __prototype__
该原型对象也有自己的原型对象`__proto__`层层向上直到有一个的原型对象为null。根据定义null没有原型并作为这个原型链的最后一个环节。
<!-- more -->
几乎所有javascript中的对象都是位于原型链顶端的`Object`的实例。
## 基于原型链的继承
### 继承属性
`javascript` 对象是动态的属性"包裹"(指自身的属性)。同时,对象还有一个指向一个原型对象的链。
当访问一个对象的属性时,不仅会在该对象上查找,也会在该对象的原型上查找,进而在该对象的原型的原型上查找,
依次层层向上查找,直到找到匹配的属性,或者到达原型链的末尾。
::: info ECMAScript标准
`obj.[[Prototype]]`符号用于指向`obj`的原型。从`ES6`开始`[[Prototype]]`
可以通过`Object.getPrototypeof()``Object.setPrototypeof()` 访问器进行访问。
等同于许多浏览器实现的属性`__proto__`
:::
从代码示例来分析继承属性:
在这个示例中,定义了一个函数`Foo` 它拥有自身属性 `a``b`
然后创建一个 `Foo` 的示例 `foo`
``` js
function Foo() {
this.a = 1
this.b = 2
}
let foo = new Foo()
Foo.prototype.c = 3
Foo.prototype.d = 4
// 输出自身的所有属性
console.log(foo) // { a: 1, b: 2 }
// 自身拥有属性 a
console.log(foo.a) // 1
// 自身拥有属性 b
console.log(foo.b) // 2
// 自身没有属性 c, 但其原型上有属性 c
console.log(foo.c) // 3
// 自身没有属性 d但其原型上有属性 d
console.log(foo.d) // 4
```
在这个示例中, 整个原型链如下
``` js
// { a: 1, b: 2 } => { c: 3, d: 4 } => Object.prototype => null
```
### 继承方法
`Javascript` 中,并没有其他基于类的语言所定义的 方法。
任何函数都可以添加到对象上做为对象的属性。
函数属性的继承与其他属性的继承没有差别。
当继承的函数被调用时,`this`指向的是当前继承的对象,而不是继承的函数所在的原型对象。
``` js
var o = {
a: 1,
f: function() {
return this.a + 1
}
}
// 此时 函数f 中的 this 指向了 o
console.log(o.f()) // 2
var p = Object.create(0)
p.a = 3
// p从o上继承了函数f 此时函数f中的 this 指向了 p
console.log(p.f()) // 4
```
## 创建对象和生成原型链
### 使用语法结构创建的对象
``` js
var o = { a: 1 }
// 原型链: o => Object.prototype => null
var arr = ['a', 'b']
// 原型链: arr => Array.prototype => Object.prototype => null
function f() {}
// 原型链: f => Function.prototype => Object.prototype => null
```
### 使用构造器创建对象
``` js
function Person() {
this.a = 1
}
Person.prototype = {
f: function () {
return this.a
}
}
var p = new Person()
// 原型链: p => Person.prototype => Object.prototype => null
```
### 使用`Object.create`创建的对象
``` js
var a = { a: 1 }
var b = Object.create(a)
// 原型链 b => a => Object.prototype => null
var c = Object.create(null)
// 原型链 c => null
```
## 扩展原型链的方法
### 构造器创建对象,原型赋值给另一个构造函数原型
``` js
function foo() {}
foo.prototype = {
a: 'foo'
}
function bar() {}
var proto = new foo()
proto.b = 'bar'
bar.prototype = proto
var p = new bar()
console.log(p.a) // foo
console.log(p.b) // bar
```
### Object.create
``` js
function foo() {}
foo.prototype = {
a: 'foo'
}
function bar() {}
var proto = Object.create(foo.prototype)
proto.b = 'bar'
bar.prototype = proto
var p = new bar()
console.log(p.a) // foo
console.log(p.b) // bar
```
``` js
function foo() {}
foo.prototype = {
a: 'foo'
}
function bar() {}
var proto = Object.create(foo.prototype, { b: 'bar' })
bar.prototype = proto
var p = new bar()
console.log(p.a) // foo
console.log(p.b) // bar
```

View File

@ -1,264 +0,0 @@
---
title: 正则表达式
lang: zh-CN
createTime: 2018/11/26 11:15:27
permalink: /article/e8qbp0dh/
author: pengzhanbo
tags:
- javascript
top: false
type: null
---
_本文正则表达式基于`javascript`不同的计算机语言对正则表达式的支持情况以及实现语法不尽相同不一定适用于其他语言。_
### 简介
`正则表达式`是一种文本模式Regular Expression是对字符串的一种匹配查找规则。可以方便的在某一文本字符串中查找、定位、替换符合某种规则的字符串。
比如说,我们想要找出一段文本中的手机号码,文本内容如下:
``` html
name:Mark tel:13800138000
name:Jhon tel:13800138888
```
很明显,在这段文本中,手机号码是以 `tel:`开头,这符合一定的规则,这样我们可以通过正则表达式来书写这个规则,然后去查找匹配:
``` js
var text = `name:Mark tel:13800138000
name:Jhon tel:13800138888`;
var result = text.match(/tel:(1\d{10})/);
// ["tel:13800138000", "13800138000", index: 0, input: "tel:13800138000", groups: undefined]
var tel = result[1];
// 13800138000
```
`/tel:(1\d{10})/` 便是所说的正则表达式。
### `RegExp`与字面量
`javascript`中,我们可以使用构造函数`RegExp` 创建正则表达式。
`new RegExp(pattern[, flags])`
``` js
var regExp = new RegExp('\\d', 'g');
```
也可以通过 字面量的方式:
``` js
var regExp = /\d/g;
```
两种创建正则表达式适用的场景有些细微的不同,一般使用`new RegExp()`来创建动态的正则表达式,使用字面量创建静态的正则表达式。
正则表达式字面量是提供了对正则表达式的编译,当正则表达式保持不变时,用字面量的方式创建正则表达式可以获得更好的性能。
_以下讨论以正则表达式字面量来创建正则表达式_
`正则表达式`一般由`元字符`和普通字符组成。
### 元字符
元字符也叫特殊字符,是正则表达式规定的,对符合特定的单一的规则的字符的描述。
| 字符 | 含义 |
|:----:|-----|
| \\ |在非特殊字符的前面加反斜杠,表示这个字符是特殊的,不能从字面上解释。比如在`\d`描述的不是一个普通的字符`d`,而是正则表达式中的数值`0-9`<br/>如果在特殊字符前面加反斜杠,这表示将这个字符转义为普通字符,比如`?`在正则中有其特殊含义,前面加反斜杠\?,这可以将其转为普通的`?`。|
|^|匹配文本开始的位置,如果开启了多行标志,也会匹配换行符后紧跟的位置。<br/>比如`^a`会匹配`abc`,但不会匹配到`bac`。|
|$|匹配文本结束的位置,如果开启了多行标志,也会匹配换行符前紧跟的位置。<br/>比如`b$`会匹配`acb`,但不会匹配到`abc`。|
|*|匹配前一个表达式0次到多次。<br/>比如,`ab*`会匹配到`abbbbbbc`中的`abbbbbb`,以及`acbbbbb`中的`a`。|
|+|匹配前一个表达式1次到多次。<br/>比如,`ab+`会匹配到`abbbbbbc`中的`abbbbbb`,但不会匹配`acbbbbb`。|
|?|匹配前一个表达式0次到1次。<br/>比如,`ab*`会匹配到`abbbbbbc`中的`ab`,以及`acbbbbb`中的`a`。|
|.| 匹配除换行符之外的任何单个字符。|
|x\|y| 匹配 x或者y。|
|\[xyz\]| 表示一个字符的集合。匹配集合中的任意一个字符。可以使用破折号`-`来指定一个字符范围。<br/>比如,`[0-4]``[01234]`,都可以匹配`4567`中的`4`。|
|\[^xyz\]| 表示一个方向字符集合。匹配任意一个不包括在集合中的字符。可以使用破折号`-`来指定一个字符范围。<br/>比如,`[0-4]``[01234]`,都可以匹配`2345`中的`5`。|
|{n}|n为一个整数表示匹配前一个匹配项n次。<br/>比如`a{2}`不会匹配`abc`中的`a`,但会匹配`aaaabc`中的`aa`。|
|{m,n}| m,n都是一个整数匹配前一个匹配项至少发生了m次最多发生了n次。<br/>当mn值为0时这个值被忽略当n值不写`{1,}`表示1次到多次。当m值不写时`{,1}`表示0次到1次。|
|(x)|匹配`x`并且捕获该匹配项。称为捕获括号,括号中的匹配项也称作子表达式。|
|(?:x)| 匹配`x`但不捕获该匹配项。称为非捕获括号。|
|x(?=y)| 匹配`x`且当`x`后面跟着`y`。称为正向肯定查找(正向前瞻)。|
|x(?!y)| 匹配`x`且当`x`后面不跟着`y`。称为正向否定查找(负向前瞻)。|
|[\b]|匹配一个退格(U+0008)。|
|\b |匹配一个词的边界。匹配的值的边界并不包含在匹配的内容中。|
|\B|匹配一个非单词的边界。|
|\d|匹配一个数字。等价于`[0-9]`。|
|\D|匹配一个非数字。等价于`[^0-9]`。|
|\n|匹配一个换行符 (U+000A)。|
|\r|匹配一个回车符 (U+000D)。|
|\s|匹配一个空白字符,包括空格、制表符、换页符和换行符。<br/>等价于`[ \f\n\r\t\v\u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]`|
|\S|匹配一个非空白字符。|
|\t|匹配一个水平制表符 (U+0009)。|
|\w|匹配一个单字字符(字母、数字或者下划线)。<br/> 等价于`[A-Za-z0-9_]`。|
|\W|匹配一个非单字字符。|
|\xhh|与代码 hh 匹配字符(两个十六进制数字)|
|\uhhhh|与代码 hhhh 匹配字符(四个十六进制数字)。|
上表在多数文章都会提及,但有一些注意的细节,下面我单独拎出来说说。
1. __`[xyz]` 匹配集合中的任意一个字符__ <br/>
这个字符集的元素,可以是普通字符,也可以是特殊字符,也可以用破折号`-`规定一个字符集范围。<br/>
以匹配数字为例,可以写成`[0123456789]` ,也可以写成`[\d]`,也可以写成`[0-9]`<br/>
类似于`()`等特殊字符,在`[]`中有其作用,都特殊字符的作用一致,不能直接当做普通字符来使用,所以我们需要使用反斜杠`\`将其转义为普通字符,值得注意的是,上表的特殊字符中,星号`*`、小数点`.``[]`中并没有特殊用途,所以不需要做转义处理,当然,即使做了转义,也不会出现问题;而破折号`-``[]`中有其特殊作用,所以作为普通字符使用时,需要转义。
2. __`?`匹配前一个表达式0次到1次。__<br/>
其实这里准确描述来说,匹配前一个表达式,且该表达式 __非任何量词 `*`、 `+`、`?` 或 `{}`__ 匹配前一个表达式0次到1次。<br/>
如果紧跟在 __非任何量词 `*`、 `+`、`?` 或 `{}`__ 的后面,将会使量词变为非贪婪的(匹配尽量少的字符)<br/>
_贪婪与非贪婪匹配我们在下文细说。_
### 等价字符
正则表达式中,有不少特殊字符的写法,是等价的,也可以说是简写形式,下表的左右两边,都是等价的。
|regExp|regExp|
|--|--|
|*|{0,}|
|+|{1,}|
|?|{0,1}|
|\d|\[0-9\]|
|\D|\[^0-9\]|
|\w|\[a-zA-Z_\]|
|\W|\[^a-zA-Z_\]|
|\s|\[ \f\n\r\t\v\u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff\]|
### 贪婪模式与非贪婪
__什么是贪婪模式__
贪婪是指正则表达式匹配时,是贪心的,会尽可能的匹配多的字符,主要体现在`量词特殊字符`
``` js
// 匹配一个到多个数字
var r = /\d+/;
var t1 = '12a';
var t2 = '1234a';
var t3 = 'a12b345';
console.log(t1.match(r)[0]); // 12
console.log(t2.match(r)[0]); // 1234
console.log(t3.match(r)[0]); // 12
```
`非贪婪`,即是让正则表达式匹配尽量少的字符。那么如何改变正则表达式的贪婪模式?
__在量词特殊字符后面紧跟使用`?`__
我们说说的量词包括`*`, `+`, `?`, `{m,n}`。那么紧跟了`?`,会有什么不同的表现呢?
我们从例子来分析:
``` js
var r1 = /<div>.*<\/div>/;
var r2 = /<div>.*?<\/div>/;
var str = '<div>aaa</div>bbb<div></div>ccc';
```
变量`r1`是贪婪匹配,得到的结果会是什么呢?
``` js
console.log(str.match(r1)[0]);
// <div>aaa</div>bbb<div></div>
```
在这段字符串中,有两个`</div>`的匹配字符串,正则表达式在遇到第一个`</div>`匹配字符项时,同时满足了`/.*/``/<\/div>/`的匹配条件,优先作为`/.*/`的匹配值,在遇到第二个时,同样还是优先作为`/.*/`的匹配值,直到匹配的字符串`str`的结束,没有满足条件的匹配字符串,再把第二个`</div>`作为`/<\/div>/`的匹配值。最终得到了`<div>aaa</div>bbb<div></div>`的匹配结果。
变量`r2`这是非贪婪匹配,得到的结果又会有所不同:
``` js
console.log(str.match(r1)[0]);
// <div>aaa</div>
```
同样,两个`</div>`的匹配字符串,但实际非贪婪匹配模式,在匹配到第一个`</div>`,就不会再继续向下匹配字符串了。
__也就是说贪婪匹配是在满足规则下尽可能多的匹配更多的字符串直到字符串结束或没有满足规则的字符串了非贪婪匹配是在满足规则下尽可能少的匹配最少的字符串一旦得到满足规则的字符串就不再向下匹配。__
1. `x*?`:尽可能少的匹配`x`匹配的结果可以是0个`x`
2. `x+?`:尽可能少的匹配`x`但匹配的结果至少有1个`x`
3. `x??`:尽可能少的匹配`x`, 匹配的结果可以是0个`x`,但最多可以有一个`x`
4. `x{m,n}?`:尽可能少的匹配`x`但匹配的结果至少有m个`x`最多可以有n个`x`
可能从字面来说,不好理解 `x??`, `x{m,n}?` ,来看一个例子就可以明白了:
``` js
var s1 = '<div>aa</div>';
var s2 = '<div>a</div>';
var s3 = '<div></div>';
var r1 = /<div>a??<\/div>/;
console.log(r1.test(s1)); // false
console.log(r1.test(s2)); // true
console.log(r1.test(s3)); // true
```
``` js
var s1 = '<div>aaa</div>';
var s2 = '<div>aa</div>';
var s3 = '<div>aaaa</div>'
var r1 = /<div>a{2,3}?<\/div>/;
var r2 = /<div>a{2,3}?/;
console.log(r1.test(s1)); // true
console.log(r1.test(s2)); // true
console.log(r1.test(s3)); // false
console.log(s1.match(r2)[0]); // <div>aa
console.log(s2.match(r2)[0]); // <div>aa
console.log(s3.match(r2)[0]); // <div>aa
```
### 正则表达式标志
|标志|描述|
|:--:| -- |
|g|全局搜索|
|i|不区分大小写搜索|
|m|多行搜索|
|y|执行“粘性”搜索,匹配从目标字符串的当前位置开始|
|u|Unicode模式。用来正确处理大于 \uFFFF 的Unicode字符|
__`m`__
使用`m`标志时,会改变开始(`^`)和结束字符(`$`)的工作模式,变为在多行上匹配,分别匹配每一行的开始和结束,即`\n``\r` 分割。
__`y`__
使用`y`标志时,匹配是从`RegExp.lastIndex`指定的位置开始匹配,匹配为真时,会修改`lastIndex`的值到当前匹配字符串后的位置,下次匹配从这个位置开始匹配,如果匹配为假时,不会修改`lastIndex`的值。
``` js
let reg = /o/y;
let str= 'foo';
// lastIndex 为 0从字符 f 开始匹配
reg.test(str); // false
// 由于结果为 false lastIndex 还是为 0
reg.test(str); // false
let str2 = 'oof';
// lastIndex 为 0 ,从字符 o 开始匹配
reg.test(str2); // true
// lastIndex 此时修改为 1, 从第二个 o 开始匹配
reg.test(str2); // true
// lastIndex 此时修改为 2
reg.test(str2); // false 此时开始匹配的字符是 f
// lastIndex没有被修改
reg.test(str2); // false
```
### 正则表达式中的捕获—— \1,\2,\3... 以及 $1,$2,$3...
在上文中我们介绍了 `(x)` 是匹配 `x` 并捕获,那么有了捕获就必然可以去使用捕获到的结果, `\1,\2,\3...` 以及` $1,$2,$3...` 便是指捕获的结果。
`\1, \2, \3, \4, \5, \6, \7, \8, \9` 在正则表达式中使用,捕获结果为正则表达式的源模式.
在这个正则表达式中`(bc)`被捕获并标记为`\1`, `(ef)`被捕获并标记为`\2`
``` js
let reg = /a(bc)d(ef)/
```
也可以使用来简化正则表达式
``` js
let reg = /a(bc)dbc/
let reg2 = /a(bc)d\1/
let str = 'abcdbc';
reg.test(str); // true
reg2.test(str); // true
```

`$1, $2, $3, $4, $5, $6, $7, $8, $9``RegExp`的包含括号子表达式的正则表达式静态的只读属性。
``` js
let reg = /a(bc)d/;
let str = 'abcd';
reg.test(str);
console.log(RegExp.$1); // bc
```
`String.replace()` 中使用:
``` js
let reg = /(\w+)\s(\w+)/;
let str = 'apple pear';
str.replace(reg, '$2 $1'); // pear apple
RegExp.$1; // apple
RegExp.$2; // pear
```

View File

@ -1,189 +0,0 @@
---
title: Event Loop 浏览器端的事件循环
createTime: 2021/06/03 01:53:17
permalink: /article/browser-event-loop/
author: pengzhanbo
tags:
- javascript
---
事件循环,即 Event-Loop。
## 什么是 Event-Loop
Event-Loop 是一个执行模型,在 [html5规范](https://html.spec.whatwg.org/multipage/webappapis.html#event-loops) 中进行了浏览器端的 Event-Loop 的明确定义。
## 宏任务与微任务
javascript 有两种异步任务,分别是`宏任务``微任务`
### 宏任务
宏任务,`macro task`,也叫 `tasks`,一些异步任务的回调会依次进入 `macro task queue`,等待后续被调用。
这些异步任务包括:
- setTimeout
- setInterval
- setImmediate (Node独有)
- requestAnimationFrame (浏览器独有)
- I/O
- UI rendering
### 微任务
微任务, `micro task`, 也叫 `jobs`,另一些异步任务的回调会依次进入`micro task queue`,等待后续被调用。
这些异步任务包括:
- process.nextTick(Node独有)
- Promise
- Object.observe
- MutationObserver
## 事件循环 Event Loop
1. 执行全局 `script` 代码,这些代码有一些是同步语句,有一些是异步语句(如: setTimeout
2. 全局`script`同步代码执行完毕后调用栈Stack会清空
3. 从微任务`micro task queue` 中取出位于队首的任务放入调用栈Stack中执行执行完后`micro task queue`长度减一;
4. 继续取出微任务`micro task queue`位于队首的任务放入调用栈Stack中执行
以此类推,直到把`micro task queue`中的所有任务都执行完毕。__注意如果在执行micro task的过程中产生了`micro task`那么会加入到队列的末尾也会在这个周期被调用执行__
5. `micro task`中的所有无人都执行完毕,此时 `micro task queue` 为空队列调用栈Stack也为空
6. 取出宏队列 `macro task queue` 中位于队首的任务放入Stack中执行
7. 执行完毕后调用栈Stack为空
8. 重复第3-7个步骤
9. 以此继续循环重复;
::: tip 重点
1. 宏任务`marco task` 一次只从队列中取出一个任务执行,执行后就去执行微任务队列中的任务;
2. 微任务队列中所有的任务都会依次取出来执行,直到`micro task queue`为空,
且当前微任务执行过程中产生新的`micro task`,也会加入到当前`micro task queue`;
3. `UI Rendering`由浏览器自定判断决定执行节点。但是只要执行`UI Rendering`,它的节点是在执行完所有
`micro task`之后,下一个`macro task`之前,紧跟着执行`UI Rendering`
:::
尝试从代码层面来分析 event-loop:
::: note 抖个机灵
代码人看代码应该比看流程图要来得好理解了吧bushi
:::
``` js
// 执行器
// 接收一段javascript代码
class Execution {
constructor(code) {
this.code = code
this.macroTaskQueue = []
this.microTaskQueue = []
}
// 启动执行
exec() {
// 首次运行,将 传入的 code 推入到 Track中执行
// 并获取其中的 宏任务和 微任务
const { macroTaskQueue, microTaskQueue } = this.run(this.code)
// 将宏任务和微任务 推入到 各自的 队列中
this.macroTaskQueue.push(...macroTaskQueue)
this.microTaskQueue.push(...microTaskQueue)
// 开始执行微任务
this.runMicroTaskQueue()
}
// 执行微任务队列
runMicroTaskQueue() {
// 遍历 微任务队列中的所有任务
// 当当前的 微任务队列清空时,遍历才结束
while (this.microTaskQueue.length) {
// 取出 队首的微任务
const task = this.microTaskQueue.shift()
// 将 当前微任务 推入到 执行栈中执行
// 并将返回的 宏任务和微任务 继续 推入到 各自的队列中
const { macroTaskQueue, microTaskQueue } = this.run(task)
this.macroTaskQueue.push(...macroTaskQueue)
this.microTaskQueue.push(...microTaskQueue)
}
// 当前微任务执行完毕,继续执行宏任务
this.runMacroTaskQueue()
}
// 执行宏任务队列
runMacroTaskQueue() {
// 从 宏任务队列队首 取出一个 宏任务
const task = this.macroTaskQueue.shift()
// 将当前 宏任务 推入到 执行栈中执行
// 并将返回的 宏任务和微任务 继续 推入到 各自的队列中
const { macroTaskQueue, microTaskQueue } = this.run(task)
this.macroTaskQueue.push(...macroTaskQueue)
this.microTaskQueue.push(...microTaskQueue)
// 再一次执行 微任务队列中的任务
this.runMicroTaskQueue()
}
// 执行栈调用
run(task) {
// track 函数表示 执行栈
// 执行完毕返回 产生的 微任务队列 和 宏任务队列
const { macroTaskQueue, microTaskQueue } = track(task)
return { macroTaskQueue, microTaskQueue }
}
}
const execute = new Execution(scriptCode)
execute.exec()
```
`event-loop` 概念性的内容大体就这么多,接下来从示例中来实际执行情况。
## 示例
::: warning 注意
以下示例是在 `Chrome` 中执行后获得的结果,在其他浏览器的表现并不一定完全相同。
:::
可以尝试自己心中执行这段代码后的打印顺序,再切换到`Console`中看实际的运行结果,是否符合你的预期结果。
::: code-tabs
@tab javascript
``` js
console.log('script')
setTimeout(() => {
console.log('timeout 1')
Promise.resolve().then(() => {
console.log('promise 1')
})
})
new Promise((resolve) => {
console.log('promise resolver')
Promise.resolve().then(() => {
console.log('promise 3')
})
resolve('promise 2')
}).then((data) => {
console.log(data)
})
setTimeout(() => {
console.log('timeout 2')
})
console.log('end')
```
@tab Console
``` txt
script
promise resolver
end
promise 3
promise 2
timeout 1
promise 1
timeout 2
```
:::

View File

@ -1,407 +0,0 @@
---
title: 详解 Promise
createTime: 2020/11/22 12:58:28
permalink: /article/q40nq4hv/
author: pengzhanbo
tags:
- javascript
---
## 概述
`Promise` 是一个构造函数,用于创建一个新的 Promise 对象。该构造函数主要用于包装还没有添加 promise 支持的函数。
``` ts
Promise(resolver : (resolve, reject) => void)
```
`Promise` 接受一个函数`resolver`作为参数,包装需要执行的处理程序,当处理结果为成功时,将成功的返回值作为参数调用`resolve` 方法,
如果失败,则将失败原因作为参数调用`reject`方法。
### 示例
``` js
const promise = new Promise(function (resolve, reject) {
setTimeout(() => {
// do something
if (Math.random() * 10 > 5) {
resolve({ status: 'success', data: '' })
} else {
reject(new Error('error'))
}
}, 500)
})
```
### Promise状态
Promise 创建后,必然处于以下几种状态
- `pending` : 待定状态,既没有被兑现,也没有被拒绝
- `fulfilled` : 操作成功。
- `rejected` : 操作失败。
当状态从 `pending` 更新为`fulfilled``rejected` 后,就再也不能变更为其他状态。
### Promise 实例方法
#### `.then(onFulfilled, onRejected)`
*then()* 接收两个函数参数(也可以仅接收一个函数参数 onFulfilled
- onFulfilled 函数参数,表示当 promise的状态从 `pending` 更新为`fulfilled` 时触发,并将成功的结果 value 作为`onFulfilled`函数的参数。
- onRejected 函数参数表示当promise的状态从 `pending` 更新为`rejected` 时触发,并将失败的原因 reason 作为 `onRejected`函数的参数。
*then()* 方法返回的结果会被包装为一个新的promise实例。
#### `.catch(onRejected)`
*catch()* 可以相当于 *.then(null, onRejected)*即仅处理当promise的状态从 `pending` 更新为`rejected` 时触发。
#### `.finally(onFinally)`
表示promise的状态无论是从`pengding`更新为`fulfilled``rejected`,当所有的 then() 和 catch() 执行完成后,最后会执行 finally() 的回调。
由于无法知道promise的最终状态`onFinally` 回调函数不接收任何参数,它仅用于无论最终结果如何都要执行的情况。
``` js
promise
.then(function (res) {
console.log(res) // { status: 'success', data: '' }
})
.catch(function (reason) {
console.log(reason) // error: error
})
.finally(() => {
// do something
})
```
### 链式调用
使用Promise的一个优势是可以链式调用的方式执行多个`then()`/`catch()`方法。
且回调函数允许我们返回任何值,返回的值将会被包装为一个 promise实例将值传给下一个`then()`/`catch()`方法。
``` js
promise
.then(res => {
res.data = { a: 2 }
return res
})
.then(res => {
console.log(res) // { status: 'success', data: { a: 2 } }
throw new Error('cath error')
})
.catch(reason => {
console.log(reason) // error: cath error
})
```
## Promise代码实现
```js
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
const microtask = globalThis.queueMicrotask || ((cb) => setTimeout(cb, 0));
function LikePromise(resolver) {
if (typeof resolver !== "function") {
throw new TypeError(`Promise resolver ${resolver} is not a function`);
}
this.state = PENDING;
this.value = undefined;
this.reason = undefined;
this.fulfillQueue = [];
this.rejectQueue = [];
const that = this;
function reject(reason) {
if (that.state === PENDING) {
that.state = REJECTED;
that.reason = reason;
that.rejectQueue.forEach((cb) => cb(reason));
}
}
function resolve(value) {
if (that.state === PENDING) {
that.state = FULFILLED;
that.value = value;
that.fulfillQueue.forEach((cb) => cb(value));
}
}
try {
resolver(resolve, reject);
} catch (e) {
reject(e);
}
}
function resolvePromise(promise, x, resolve, reject) {
if (promise === x) {
reject(new TypeError("chaining cycle"));
} else if (x !== null && (typeof x === "object" || typeof x === "function")) {
let used = false;
try {
const then = x.then;
if (typeof then === "function") {
then.call(
x,
(y) => {
if (used) return;
used = true;
resolvePromise(promise, y, resolve, reject);
},
(r) => {
if (used) return;
used = true;
reject(r);
}
);
} else {
if (used) return;
used = true;
resolve(x);
}
} catch (e) {
if (used) return;
used = true;
reject(e);
}
} else {
resolve(x);
}
}
LikePromise.prototype.then = function (onFulfilled, onRejected) {
onFulfilled =
typeof onFulfilled === "function" ? onFulfilled : (value) => value;
onRejected =
typeof onRejected === "function"
? onRejected
: (reason) => {
throw reason;
};
const that = this;
const promise = new LikePromise((resolve, reject) => {
if (that.state === FULFILLED) {
microtask(() => {
try {
const x = onFulfilled(that.value);
resolvePromise(promise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
} else if (that.state === REJECTED) {
microtask(() => {
try {
const x = onRejected(that.reason);
resolvePromise(promise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
} else {
that.fulfillQueue.push(() => {
microtask(() => {
try {
const x = onFulfilled(that.value);
resolvePromise(promise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
});
that.rejectQueue.push(() => {
microtask(() => {
try {
const x = onRejected(that.reason);
resolvePromise(promise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
});
}
});
return promise;
};
LikePromise.prototype.catch = function (onRejected) {
return this.then(null, onRejected);
};
LikePromise.prototype.finally = function (onFinally) {
return this.then(
(value) => LikePromise.resolve(onFinally()).then(() => value),
(reason) =>
LikePromise.resolve(onFinally()).then(() => {
throw reason;
})
);
};
LikePromise.resolve = function (value) {
return new LikePromise((resolve) => resolve(value));
};
LikePromise.reject = function (reason) {
return new LikePromise((_, reject) => reject(reason));
};
```
## `Promise` 静态方法
### Promise.resolve(value)
返回一个状态由给定value决定的Promise对象。如果该值是thenable(即带有then方法的对象)返回的Promise对象的最终状态由then方法执行决定否则的话(该value为空基本类型或者不带then方法的对象),返回的Promise对象状态为fulfilled并且将该value传递给对应的then方法。通常而言如果您不知道一个值是否是Promise对象使用Promise.resolve(value) 来返回一个Promise对象,这样就能将该value以Promise对象形式使用。
### Promise.reject(reason)
返回一个状态为失败的Promise对象并将给定的失败信息传递给对应的处理方法
### Promise.all(promises)
`all()` 允许传入一组promise实例并返回一个新的promise实例。
promises并发执行并且当这组promises的最终状态均更新为`fulfilled`才触发返回的promise实例的`onFulfilled`
并将这组promises的执行结果已promises的定义顺序以数组的形式传给`onFulfilled`
如果其中某个promise的最终状态更新为`rejected`则立即触发返回的promise实例的`onRejected`
#### 示例:
``` js
const promises = [
Promise.resolve({ a: 1}),
new Promise((resolve) => {
setTimeout(() => {
resolve({ b: 1 })
}, 0)
})
]
Promise.all(promises).then(res => {
console.log(res) // [ { a: 1}, { b: 1 } ]
})
```
#### 手写Promise.all 实现代码
``` js
function promiseAll(promises) {
promises = promises || []
let length = promises.length
if (length === 0) return Promise.resolve([])
let count = 0
const list = []
return new Promise((resolve, reject) => {
const resolveFn = (res, index) => {
list[index] = res
count ++
if (count >= length) {
resolve(list)
}
}
promises.forEach((item, i) => {
if (item instanceof Promise) {
item.then(res => resolveFn(res, i), reject)
} else {
resolveFn(item, i)
}
})
})
}
```
### Promise.allSettled
`allSettled(promises)` 允许传入一组promise实例并返回一个新的promise对象。
当这组promises的状态从`pending` 都更新到最终状态、无论最终状态是 `fulfilled``rejected`触发返回的promise的`onfulfilled`
`onfulfilled` 回调函数根据promises定义的顺序将执行结果以 `{ status: string, [value|reason]: any }[]` 的形式作为参数传入。
#### 示例
``` js
const promises = [
Promise.resolve({ a: 1}),
Promise.reject('reason')
]
Promise.allSettled(promises).then(res => {
console.log(res) // [ { status: 'fulfilled, value: { a: 1 } }, { status: 'rejected', reason: 'reason' } ]
})
```
#### 手写Promise.allSettled 实现代码
``` js
function promiseAllSettled(promises) {
promises = promises || []
let length = promises.length
if (length === 0) return Promise.resolve([])
let count = 0
const list = []
return new Promise((resolve) => {
const resolveFn = (res, index, status) => {
list[index] = { status }
if (status === 'fulfilled') {
list[index].value = res
} else {
list[index].reason = res
}
count ++
if (count >= length) {
resolve(list)
}
}
promises.forEach((item, i) => {
if (item instanceof Promise) {
item.then(
res => resolveFn(res, i, 'fulfilled'),
reason => resolveFn(reason, i, 'rejected')
)
} else {
resolveFn(item, i, 'fulfilled')
}
})
})
}
```
### Promise.race
`Promise.race(promises)` 接收一组promise实例作为参数并返回一个新的promise对象。
当这组promises中的任意一个promise的状态从`pending`更新为`fulfilled``rejected`返回的promise对象将会把该promise的成功返回值或者失败原因
作为参数调用返回的promise的`onFulfilled``onRejected`
#### 示例
``` js
const promises = [
new Promise((resolve) => {
setTimeout(() => {
resolve('timeout')
}, 500)
}),
Promise.resolve('resolve')
]
Promise.race(promises).then(res => {
console.log(res) // resolve
})
```
#### 手写Promise.race 实现代码
``` js
function promiseRace(array) {
array = array || []
return new Promise((resolve, reject) => {
array.forEach(item => {
if (item instanceof Promise) {
item.then(resolve, reject)
} else {
resolve(item)
}
})
})
}
```
## 参考资料
> [Promise A+ 规范](https://malcolmyu.github.io/2015/06/12/Promises-A-Plus/)
>
> [MDN Promise](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise)
>
> [MDN 使用Promise](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Using_promises)

View File

@ -1,247 +0,0 @@
---
title: 1px解决方案
createTime: 2019/05/15 10:41:32
permalink: /article/tz7ncicn/
author: pengzhanbo
tags:
- html
- css
- develop
---
在日常移动端前端应用开发中,经常遇到一个问题就是 1px的线在移动端 Retina屏下的渲染并未达到预期。以下总几种不同场景下的 1px解决方案。
<!-- more -->
## 背景及原因
首先,需要明确的一个概念是, CSS的 `pixels` 并不完全等价于 设备的 `pixels`。当我们假定设备的 `pixels` 为标准的`pixels` 宽度。这些pixels决定了设备的分辨率。在默认情况下 PC设备上用户未进行缩放操作即zoom缩放为100%时), CSS的`pixels`与设备的`pixels`重叠当用户进行了缩放操作时假设用户缩放了200%,那么 124px的CSS`pixels`实际占用了248设备`pixels`
但我们开发时,通常设备的`pixels`对我们毫无用处前端只需要关注CSS的`pixels`浏览器会根据用户缩放自动处理CSS的pixels是被伸展还是收缩。
但在移动端设备中由于设备的宽度较小导致了可显示的内容要少得多。浏览器或者缩放变小导致内容无法阅读或者通过拖动来浏览未被显示的内容。这导致了原本适合于PC设备的CSS布局放到了移动端变得十分丑陋。
为了解决这个问题移动端设备的厂商的通常做法是让viewport更宽这里的viewport指的是设备的视窗它决定了HTML标签的宽度表现继而影响其他的元素
移动端的 viewport 被分为了 虚拟的 viewport 和 布局的 viewport
- `visual viewport` 虚拟viewport
- `layout viewport` 布局viewport
![](/images/viewport.jpg)
两者的概念, 可以想象 `layout viewport` 为一张不可改变大小和角度的图片,但它被一层蒙板挡住了, `visual viewport` 是一个蒙板上我们可以观察到 这张图片的窗口。我们可以通过这个窗口观察到 图片的部分内容。并且可以对这个窗口进行拖动或缩放,进而观察到图片的完整内容。
在这里,`visual viewport` 相当于 移动端设备的屏幕,用户的 缩放和拖动操作,反馈到 `layout viewport` ,则是相对的变成 `layout viewport` 被 拖动和缩放。
而通常我们关注的 CSS`pixels`,通常是按照 `layout viewport`来定义的,所以会比`visual viewport` 宽很多。而 `<html>`元素的宽度继承于`layout viewport`。这可以保证你的网站的UI可以在移动端设备和桌面设备表现一致。
但是 `layout viewport`的宽度有多宽,不同的设备,不同的浏览器各有不同。如 iPhone 的Safari 使用的是 980px。
但是在移动端的交互中,我们并不期望 网站的内容是被缩放的,需要让用户进行缩放和拖动。 所以通常我们会在 html文件的head中进行一个 meta声明。
``` html
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
```
即强制设置了`layout viewport` 等于 设备宽度, 设置了缩放为100%并且用户无法进行缩放操作。这样做的好处是我们可以以一种期望的方式进行设计UI和交互。
但在前面,我们介绍了, CSS的`pixels`并不等价于设备的`pixels`。通常在移动端设备,我们可以通过 `window.devicePixelRatio` 查看当前设备的CSS`pixels`和设备`pixels`的比例,如 `window.devicePixelRatio` 值为 2时 表示 1个CSS`pixels`的宽度占用2个设备`pixels`,即实际占用了 2x2 的设备`pixels`
这也是导致了 `1px`的线,在移动设备上的渲染,看起来会比实际上的 `1px`更粗的原因。
知道了问题的背景,和产生的原因,那么只需要让 `1px`的 CSS`pixels`的表现,接近于或者贴合 `1px`的设备`pixels`, 那么就可以解决这个问题了。
## 解决方案
如何让 `1px`的 CSS`pixels`的表现,接近于或者贴合 `1px`的设备`pixels`。这个问题需要具体场景具体分析。
### border-width: 0.5px
一种最简单的,且适合各种场景的方案,就是使用 `0.5px` 的值代替 `1px` 的值。 但这个方案有一个兼容问题,现代浏览器并不全都支持该值的。
可以先检查是否支持 `0.5px`,然后在 根元素上添加一个 类,进行使用。
``` js
if (window.devicePixelRatio && devicePixelRatio >= 2) {
var testElem = document.createElement('div');
testElem.style.border = '.5px solid transparent';
document.body.appendChild(testElem);
if (testElem.offsetHeight == 1)
{
document.querySelector('html').classList.add('hairlines');
}
document.body.removeChild(testElem);
}
```
``` css
div {
border: 1px solid #bbb;
}
.hairlines div {
border-width: 0.5px;
}
```
这种方案的好处是简单能够适配所有场景但是从兼容性上看iOS7及之前的版本、Android设备等均不支持`0.5px`的渲染。
### 伪类 + transform缩放
该方法是是利用 元素的伪类进行线的渲染。
比如 利用 `::before` 或者 `::after`, 画一条上边框的线
``` css
.hairlines {
position: relative;
}
.hairlines::before {
content: '';
position: absolute;
left: 0;
top: 0;
display: block;
width: 100%;
height: 1px;
background-color: #000;
transform: scaleY(0.5);
transform-origin: 0 0;
}
```
比如,利用 `::before` 或者 `::after`, 画一个线框:
``` css
.hairlines {
position: relative;
}
.hairlines::before {
content: '';
position: absolute;
left: 0;
top: 0;
box-sizing: border-box;
display: block;
width: 200%;
height: 200%;
border: 1px solid #000;
transform: scale(0.5);
transform-origin: 0 0;
}
```
该方案的好处同样能都适配多数场景,并且支持圆角的情况。
但缺点在于由于对元素本身设置了`position`,以及使用了伪类,但另一个交互需要使用到被占用的属性时,需要分情况处理问题。
### border-image 图片
使用 border-image-slice 对边框图片进行偏移。
该方案的方法,比如处理 x轴方向的线时 需要准备 一张2px高的图片根据显示是上边框还是下边框如上边框则该图片的 上一半1px为对应的颜色的先下一半为透明。
_line.png_ ![1px-lines.png](/images/1px-lines.png)
``` css
div {
border-top: 1px transparent;
border-image: url(line.png) 2 0 0 0 repeat;
}
```
同理,处理其他方向的边框类似方法。
该方法的缺点是 如果改变颜色,或者有不同颜色的线,需要准备多张图片。
优先是适合多数的场景,且不对元素本身做出影响文档流的改动。
### SVG
由于CSS也支持 SVG 作为 image 资源使用且SVG是矢量图片能够相比于使用jpg、png格式的图片获得更好的保真。
可以配合 CSS 的 `background-image` 或者 `border-image` 满足不同场景的需要。
建议此方案配合 CSS 预渲染,如`stylus/sass/less` 进行使用, 也可使用`postcss` 相关插件使用。
如在 stylus中
``` stylus
// 画一个元素的线框
borderXY(color = #eee, radius = 8px) {
$r = unit(radius/ 2, '');
border-radius radius /*px*/
background-image url(s("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' width='100%' height='100%' viewBox='0 0 200% 200%' preserveAspectRatio='xMidYMid meet'><rect fill='rgba(0,0,0,0)' width='100%' height='100%' stroke-width='1' stroke='%s' rx='%s' ry='%s'/></svg>", color, $r, $r))
background-repeat no-repeat
background-position 0 0
background-size 100% 100%
}
// 画一个元素的 上下边框
borderX(color = #eee) {
border 0
border-top: 1px solid color; /*no*/
border-bottom: 1px solid color; /*no*/
border-image: url(s("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' height='200' width='100'><line x1='0' y1='25' x2='100' y2='25' stroke='%s' style='stroke-width:50'/><line x1='0' y1='75' x2='100' y2='75' style='stroke:transparent;stroke-width:50'/><line x1='0' y1='125' x2='100' y2='125' style='stroke:transparent;stroke-width:50'/><line x1='0' y1='175' x2='100' y2='175' stroke='%s' style='stroke-width:50'/></svg>", color, color)) 100 0 100 0 stretch;
}
// 画一个元素的 左右边框
borderY(color = #eee) {
border 0
border-left: 1px solid color; /*no*/
border-right: 1px solid color; /*no*/
border-image: url(s("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' height='100' width='200'><line x1='25' y1='0' x2='25' y2='100' stroke='%s' style='stroke-width:50'/><line x1='75' y1='0' x2='75' y2='100' style='stroke:transparent;stroke-width:50'/><line x1='125' y1='0' x2='125' y2='100' style='stroke:transparent;stroke-width:50'/><line x1='175' y1='0' x2='175' y2='100' stroke='%s' style='stroke-width:50'/></svg>", color, color)) 0 100 0 100 stretch;
}
// 画一个元素的上边框
borderTop(color = #eee) {
border 0
border-top: 1px solid color; /*no*/
border-image: url(s("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' height='100' width='100'><line x1='0' y1='25' x2='100' y2='25' stroke='%s' style='stroke-width:50'/><line x1='0' y1='75' x2='100' y2='75' style='stroke:transparent;stroke-width:50'/></svg>", color)) 100 0 0 0 stretch;
}
// 画一个元素的下边框
borderBottom(color = #eee) {
border 0
border-bottom: 1px solid color; /*no*/ // 设置border 0后如果color设置为transparent则该边框会变成透明
border-image: url(s("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' height='100' width='100'><line x1='0' y1='25' x2='100' y2='25' style='stroke:transparent;stroke-width:50'/><line x1='0' y1='75' x2='100' y2='75' stroke='%s' style='stroke-width:50'/></svg>", color)) 0 0 100 0 stretch;
}
// 画一个元素的左边框
borderLeft(color = #eee) {
border 0
border-left: 1px solid color; /*no*/
border-image: url(s("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' height='100' width='100'><line x1='25' y1='0' x2='25' y2='100' stroke='%s' style='stroke-width:50'/><line x1='75' y1='0' x2='75' y2='100' style='stroke:transparent;stroke-width:50'/></svg>", color)) 0 0 0 100 stretch;
}
// 画一个元素的右边框
borderRight(color = #eee) {
border 0
border-right: 1px solid color; /*no*/
border-image: url(s("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' height='100' width='100'><line x1='25' y1='0' x2='25' y2='100' style='stroke:transparent;stroke-width:50'/><line x1='75' y1='0' x2='75' y2='100' stroke='%s' style='stroke-width:50'/></svg>", color)) 0 100 0 0 stretch;
}
div {
borderXY()
}
```
如果是使用 `postcss` ,可以使用安装插件 [postcss-write-svg](https://github.com/csstools/postcss-write-svg),配合使用
``` css
@svg square {
@rect {
fill: var(--color, black);
width: var(--size);
height: var(--size);
}
}
.example {
background: svg(square param(--color green) param(--size 100%)) center / cover;
}
```
使用SVG的优点是支持调整线的颜色支持设置圆角可以根据场景不同选择 `background-image` 或者 `border-image` 满足绝大多数的场景。
### background-image + jpg/png 图片
该做法是使用一张该元素的多倍的背景图,进行线的渲染。
该做法一般不推荐。
### 背景渐变
该方案不推荐
### box-shadow
该方案不推荐

View File

@ -1,121 +0,0 @@
---
title: lerna使用
createTime: 2021/11/26 06:28:37
permalink: /article/i1wc1uld/
author: pengzhanbo
---
::: center
![lerna](https://user-images.githubusercontent.com/645641/79596653-38f81200-80e1-11ea-98cd-1c6a3bb5de51.png){ style="width: 50%" }
:::
## 概述
`lerna` 是一个多包管理工具,针对使用 git 和 npm/yarn 等管理多软件包的代码仓库的工作流程进行优化。
在开发一个大型项目时,往往会将整个项目拆分为多个代码仓库,进行独立版本化的软件包管理,这对于代码共享非常有用。
比如开源项目 `babel`,整个项目被拆分为了`@babel/core`, `@babel/parser`, `@babel/traverse`等多个软件包。
但是这也会导致如果某些更改跨越了多个代码仓库的话,会变得麻烦且难以跟踪。
`lerna`可以帮助优化对多个代码仓库的依赖、版本管理、工作流等。
## 安装
lerna 可以全局安装,也可以在项目中安装(以下内容使用项目中安装的方式)
``` sh
# npm
npm install lerna
# yarn
yarn add lerna
```
## 简单入门
创建一个项目并使用lerna进行项目环境初始化
``` sh
mkdir lerna-demo && cd $_
yarn init -y
yarn add lerna
npx lerna init
```
你将会得到一个包含以下内容的项目文件夹:
``` sh
lerna-demo
packages/
lerna.json
package.json
```
其中,`packages/` 目录用于存放所有的软件包。`lerna.json`是lerna的配置文件。
## 配置说明
``` json
// lerna.json
{
"useWorkspaces": true,
"npmClient": "npm", // npm | yarn
"packages": ["packages/*"],
"version": "0.0.0",
"command": {
"bootstrap": {
// more...
},
// more
}
}
```
- `npmClient`:设置当前使用的包管理器, 默认是npm 可以设置为yarn
- `version`:软件包版本号,根据 semver版本号规范命名
- `packages`软件包所在的目录可以使用golb做模式匹配
- `useWorkspaces`使用工作空间这个选项可以更好的跟yarn配合使用
- `command`对lerna的各个command进行配置。
## 命令行说明
### lerna init
初始化一个lerna项目默认将会在目录中新建 packages/ 和 lerna.json。
`--independent`: 使用分包独立版本管理模式,各个软件包使用独立的版本号。
### lerna create pkgName [location]
在项目中新建一个子包, pkgName设置包名。 location制定包所在目录默认是 packages配置的第一个元素。
### lerna add \<package\>[@version] [--dev] [--exact] [--peer]
类似于 `yarn add``npm install`在一个lerna repo中往dependency中添加依赖包。
- `--dev`: 表示将包添加到 devDependencies
- `--exact`: 添加一个确定版本的包如1.0.1),而不是一个版本范围的包如(^1.0.1
- `--peer`: 添加一个前置依赖包。
### lerna bootstrap
为当前 lerna repo 中的所有包安装 依赖库,并 link所有 同域依赖。
### lerna run \<script\>
在当前 lerna repo 中的所有包中执行 script 命令。
``` sh
packages/
package1/
package2/
```
``` sh
lerna run build # 相当于在 package1、package2 中执行 npm run build
```
- --scope 过滤符合条件的包
``` sh
lerna run build --scope test component
```
- --stream 使用报名作为前缀,交叉输出所有包的控制台信息流。
``` sh
lerna run build --stream
```
- --parallel 类似于 stream。
``` sh
lerna run build --parallel
```
### lerna clean
删除所有包的node_modules

View File

@ -1,280 +0,0 @@
---
title: pnpm 包管理器
createTime: 2022/05/10 03:50:47
author: pengzhanbo
permalink: /article/84nu27cz/
---
`pnpm` 是一款新兴不久的包管理器,相比于 `npm``yarn``pnpm` 拥有更快的安装速度,同时节约磁盘空间。
<!-- more -->
## 介绍
![pnpm](https://pnpm.io/zh/img/pnpm-no-name-with-frame.svg)
`pnpm` 是一个类似于 `npm``yarn` 的包管理器。
`pnpm` 安装的包都会被存储在硬盘的某个相同位置,软甲包通过硬链接到这个位置,实现共享同一版本的依赖,
对于同一依赖的不同版本,`pnpm update` 时,只会向存储中心添加新版本更新的文件,而不是仅仅应为一个文件的改变而复制整个新版本包的内容。
`pnpm` 内置支持 `monorepo`,即单仓库多包。
## 比较
### pnpm/yarn/npm
| 功能 | pnpm | yarn | npm |
| -- | :--: | :--: | :--: |
| 工作空间支持monorepo| ✔️ | ✔️ | ✔️ |
| 隔离的`node_modules` | ✔️ - 默认 | ✔️ | ❌ |
| 提升的`node_modules` | ✔️ | ✔️ | ✔️ -默认 |
| 自动安装peers | ✔️ - `auto-install-peers=true` | ❌ | ✔️ |
| Plug'n'Play | ✔️ | ✔️ - 默认 | ❌ |
| 零安装 | ❌ | ✔️ | ❌ |
| 修复依赖项 | ✔️ | ✔️ | ❌ |
| 管理nodejs版本 | ✔️ | ❌ | ❌ |
| 有锁文件 | ✔️ - `pnpm-lock.yaml` | ✔️ - `yarn.lock` | ✔️ - `package-lock.json` |
| 支持覆盖 | ✔️ | ✔️ - `resolutions` | ✔️ |
| 内容可寻址存储 | ✔️ | ❌ | ❌ |
| 动态包执行 | ✔️ - `pnpm dlx` | ✔️ - `yarn dlx` | ✔️ - `npx` |
| Side-effects cache | ✔️ | ❌ | ❌ |
### 区别
`yarn/npm` 不同的是,`pnpm` 并非采用 *扁平的`node_modules`* 来管理依赖项,
而是基于符号链接的`node_modules` 结构。
`node_modules` 中每个包的每个文件都是来自内容可寻址存储的硬链接。 假设安装了依赖于 `bar@1.0.0``foo@1.0.0``pnpm` 会将两个包硬链接到 `node_modules` 如下所示:
```sh
node_modules
└── .pnpm
├── bar@1.0.0
│ └── node_modules
│ └── bar -> <store>/bar
│ ├── index.js
│ └── package.json
└── foo@1.0.0
└── node_modules
└── foo -> <store>/foo
├── index.js
└── package.json
```
这是 `node_modules` 中的唯一的“真实”文件。 一旦所有包都硬链接到 `node_modules`
就会创建符号链接来构建嵌套的依赖关系图结构。
`bar` 将被符号链接到 `foo@1.0.0/node_modules` 文件夹,然后处理依赖关系,`foo` 将被符号链接至根目录的 `node_modules` 文件夹:
```sh
node_modules
├── foo -> ./.pnpm/foo@1.0.0/node_modules/foo
└── .pnpm
├── bar@1.0.0
│ └── node_modules
│ └── bar -> <store>/bar
└── foo@1.0.0
└── node_modules
├── foo -> <store>/foo
└── bar -> ../../bar@1.0.0/node_modules/bar
```
这种布局的好处在于,只有真正在依赖项中的包才能访问。
如果是平铺的 `node_modules` 结构,所有被提升的包都可以访问。
### 优势
- 节约磁盘空间
包存储在全局存储中pnpm 创建从全局存储到项目下 `node_modules` 文件夹的 硬链接,硬链接指向磁盘上原始文件所在的同一位置。不同软件包可以共享相同依赖项所占用的空间。
如果是单个依赖的不同版本,如版本更新,`pnpm` 仅安装版本更新的文件,而不是全量安装整个新版本的包。
- 安装速度快
软件包中安装依赖时,如果检索到在本地的全局存储中已安装过该依赖,那么不会从网络下重新安装,而是直接创建硬链接到软件包中。
- 内置支持 monorepo
支持 单仓库多包,通过 `pnpm-workspace.yaml` 配置工作空间,通过 `workspace:*` 协议引用工作空间的依赖包。
## 安装
### 通过 npm 安装
```sh
npm install -g pnpm
```
### 通过 Corepack 安装
从 v16.13 开始Node.js 发布了 `Corepack` 来管理包管理器。 这是一项实验性功能,因此需要通过运行如下脚本来启用它:
```sh
corpack enabled
```
这将自动在系统上安装 pnpm。 但是,它可能不是最新版本的 pnpm。
若要升级,请检查 [最新的 pnpm 版本](https://github.com/pnpm/pnpm/releases/tag/v7.9.1) 并运行:
```sh
corepack prepare pnpm@<version> --activate
```
### 使用独立脚本安装
在 POSIX 系统上,即使没有安装 Node.js也可以使用以下脚本安装 pnpm
```sh
curl -fsSL https://get.pnpm.io/install.sh | sh -
```
如果没有安装 `curl` ,也可以使用 `wget`:
```sh
wget -qO- https://get.pnpm.io/install.sh | sh -
```
在 Windows 系统中,如果使用 Powershell:
```sh
iwr https://get.pnpm.io/install.ps1 -useb | iex
```
### 使用 Homebrew 安装
```sh
brew install pnpm
```
### 使用 Scoop 安装
```sh
scoop install nodejs-lts pnpm
```
## 使用
`pnpm` 在使用上 与 `npm``yarn` 的使用上差别不大,但需要注意的区别,`pnpm` 会严格校验所有参数,
比如,`pnpm install --target_arch x64` 会执行失败,因为 `--target_arch x64` 不是 `pnpm install` 的有效参数。
### 常用命令
#### `pnpm install`
别名 `pnpm i`
等效于 `npm install` / `yarn`
用于安装项目所有依赖。
[pnpm install 官方文档](https://pnpm.io/zh/cli/install)
#### `pnpm add <pkg>`
安装软件包及其依赖的任何软件包。 默认情况下,任何新软件包都安装为生产依赖项。
[pnpm add 官方文档](https://pnpm.io/zh/cli/add)
#### `pnpm remove`
别名: `rm` `uninstall` `un`
`node_modules` 和项目的 `package.json` 中删除相关 packages。
[pnpm remove 官方文档](https://pnpm.io/zh/cli/remove)
#### `pnpm update`
别名: `up` `upgrade`
`pnpm update` 根据指定的范围更新软件包的最新版本。
在不带参数的情况下使用时,将更新所有依赖关系。 您可以使用一些模式来更新特定的依赖项。
[pnpm update 官方文档](https://pnpm.io/zh/cli/update)
更多命令请查阅[官方文档](https://pnpm.io/zh/cli/add)
### 配置
#### `.npmrc`
`pnpm` 从命令行、环境变量和 `.npmrc` 文件中获取其配置。
`pnpm config` 命令可用于更新和编辑 用户和全局 .npmrc 文件的内容。
四个相关文件分别为:
- 每个项目的配置文件(`/path/to/my/project/.npmrc`
- 每个工作区的配置文件(包含 `pnpm-workspace.yaml` 文件的目录)
- 每位用户的配置文件( `~/.npmrc`
- 全局配置文件( `/etc/.npmrc`
#### `pnpm-workspace.yaml`
`pnpm-workspace.yaml` 定义了 工作空间 的根目录,并能够使工作空间中包含 / 排除目录 。 默认情况下,包含所有子目录。
``` yaml
packages:
# 定义 packages 目录下的所有子目录都为一个 package
- 'packages/*'
# 定义 components 目录下的所有子目录都为一个 package
- 'components/**'
# 排除任何目录中的 test 目录下的所有目录
- '!**/test/**'
```
## 工作空间
pnpm 内置了对单一存储库(也称为多包存储库、多项目存储库或单体存储库)的支持, 你可以创建一个 workspace 以将多个项目合并到一个仓库中。
一个 workspace 的根目录下必须有 `pnpm-workspace.yaml` 文件, 也可能会有 `.npmrc` 文件。
### Workspace 协议 (workspace:)
默认情况下,如果可用的 packages 与已声明的可用范围相匹配pnpm 将从工作区链接这些 packages。 例如,如果 `bar` 中有 `"foo""^1.0.0"` 的这个依赖项,则 `foo@1.0.0` 链接到 `bar。` 但是,如果 `bar` 的依赖项中有 `"foo": "2.0.0"`,而 `foo@2.0.0` 在工作空间中并不存在,则将从 `npm registry` 安装 `foo@2.0.0` 。 这种行为带来了一些不确定性。
幸运的是pnpm 支持 workspace 协议 `workspace:` 。 当使用此协议时pnpm 将拒绝解析除本地 workspace 包含的 package 之外的任何内容。 因此,如果设置为 `"foo": "workspace:2.0.0"` 时,安装将会失败,因为 `"foo@2.0.0"` 不存在于此 workspace 中。
使用示例:
工作空间中存在以下项目:
```sh
+ packages/
+ foo/
+ bar/
+ qar/
+ zoo/
```
如果各个项目以其目录名作为其 package name那么可以在其他项目中如下引入依赖
``` json
{
"dependencies": {
"foo": "workspace:*",
"bar": "workspace:~",
"qar": "workspace:^",
"zoo": "workspace:^1.5.0"
}
}
```
::: tip
引入依赖的包名,是由包的 `package.json name` 确定,而不是 workspace 目录下的目录名确定。
:::
### 发布 Workspace
当以上示例进行发布时,会被转换为
```json
{
"dependencies": {
"foo": "1.5.0",
"bar": "~1.5.0",
"qar": "^1.5.0",
"zoo": "^1.5.0"
}
}
```
这个功能允许你发布转化之后的包到远端,并且可以正常使用本地 workspace 中的 packages而不需要其它中间步骤。包的使用者也可以像常规的包那样正常使用且仍然可以受益于语义化版本。

View File

@ -1,135 +0,0 @@
---
title: 前端路由
createTime: 2019/09/13 04:13:56
author: pengzhanbo
permalink: /article/xhb2iacu/
---
在现代前端中SPA应用是一种主流的前端应用交互方案其中前端路由是实现SPA应用的关键技术之一。
<!-- more -->
## 路由
**路由Router** 一般指的是 URI 中 pathname + basename + hash + query 所组成的 路径。
在前端中, **路由** 一般指的是 **随着浏览器中的地址栏的变化,呈现不同的内容给用户**
浏览器地址栏的变化,即是 访问链接的变化,具体指的就是 pathname + basename + hash + query 部分的变化。
示例:
```
/a/b
/a/b/#/hash
/a/b/#/hash?c=1
```
对于前端路由,一般会选择 监听 hash 部分的变化, 或者监听 pathname 部分的变化,从而一般有两种路由模式:
### hash 模式
通过监听 地址栏中 hash 部分的变化,从而呈现不同的内容。
::: center
**https://example.com/index.html ==#/a/b/==**
:::
在浏览器中,通过注册 **hashchange** 事件,监听 **hash** 变化。
``` js
window.addEventListener('hashchange', () => {
const hash = window.location.hash;
console.log(hash) // #/a/b/
})
```
通过 hash 实现路由的优势在于, hash 仅依赖于浏览器且hash的变化不会直接导致页面刷新天然适合于实现 前端路由。
### history 模式
通过监听 地址栏中 pathname 部分的变化,从而呈现不同的内容。
history模式是依赖于 浏览器端的 History API 而实现。
History API 允许我们对浏览器会话历史记录进行访问并操作。
::: center
**https://example.com ==/a/b/==**
:::
History API 通过 history.pushState() 和 history.replaceState() 方法,新增或者替换历史记录,
通过 popState 事件监听历史记录的变化。
直接操作历史记录的变化,结果会改变浏览器地址栏的显示内容,但不会引起浏览器刷新页面。
但是由于变化的部分一般是 `pathname + basename` 的部分,如果手动刷新页面,可能会导致浏览器通过当前路径
向服务器发起请求找不到对应的资源而返回404所以一般需要在服务器端的HTTP服务器进行配置将相关的路径请求资源
都指向同一个html资源文件。
> [History API](https://developer.mozilla.org/zh-CN/docs/Web/API/History_API)
### 其他模式
除了上述两种一般用于浏览器端中的路由模式,为了满足其他的场景,比如 在SSR场景下需要在服务端模拟路由在生成页面内容
或者在 electron 桌面应用中。一般会基于 memory 实现一种 仅通过 memory 的变化的路由的模式。
在这个模式中,通过一个普通的 JavaScript 字符串或者对象,来实现模拟 路由路径地址以及相关功能。
## Router解析
前端路由在不同的库或者框架中实现,一般会采用一套通用的解析规则,在实现细节上有所差异。
一个路由地址,一般包含如下几个部分:
- **path** 表示路由的路径
- **params** 表示路由的路径动态匹配结果
- **query** 表示路由携带的参数,未解析前为 queryString, 解析后为 queryObject
如一个 路由地址: `/a/b/?c=1` 中, `/a/b/` 部分一般称为 **path** `?c=1` 部分一般被称为 `query`
### 具名路由
具名路由,也称 静态路由 指在声明一个路由时,对地址栏路径地址使用 全等匹配,仅当声明的路由与路径地址全等时,才命中路由。
``` js
// 浏览器地址栏: https://example.com/a/b/
// 声明路由:
const routes = [
{
path: '/a/b', // 命中当前路由
},
{
path: '/a/c', // 不一致,未命中
},
{
path: '/a', // 不一致,未命中
},
{
path: '/a/b/c', // 不一致,未命中
}
]
```
### 路由匹配
路由匹配,指通过 一套匹配规则,对地址栏路径地址 进行 规则匹配,当命中匹配规则时,则命中路由。
一般场景下, 通过 `/:pathname` 的格式来表示路由路径中的动态部分。
`/user/:id` 则可以匹配 `/user/123``/user/456` 等满足规则的地址栏路径。
`/:pathname` 部分会被解析到 `params` 对象中,如上述的 通过`/user/:id`规则解析 `/user/123`,表示为:
```js
const currentRoute = {
path: '/user/123',
params: { id: 123 }
}
```
### 其他
在不同的框架或库中, 对路由解析会在基于上述的规则的基础上,进行补充和扩展,提供更加丰富的功能,以满足更多的场景。
比如, **Vue-Router** 使用了 `path-to-regexp` 库作为其路由解析的依赖,该库提供了非常丰富且灵活的路径匹配功能,
能够适配非常多的从简单到复杂的场景。**React-Router** 则在其内部实现了和扩展了相关的规则。

View File

@ -1,285 +0,0 @@
---
title: 谈谈微前端
createTime: 2019/08/31 05:13:33
author: pengzhanbo
permalink: /article/vpqgx0t7/
---
微前端 是最近比较新兴的一个话题,它不具体指某个库某个框架,而是一个思想,一种概念,运用这种思想,
根据自身的需求,从而实现适用于自身的 微前端 。
<!-- more -->
> 本文根据最近我在公司内部举行的 微前端技术解决方案 分享而写。
> 提供的 微前端方案 也应用于公司内部的项目,并取得了良好的反馈,获得广泛好评。
> 本文不具体谈如何实现微前端,仅讲述微前端的概念,期望能够通过本文理解微前端。
## 前言
微前端 目前在行业内是一个新兴的思想。
诞生这个思想的背景是,在公司内部,常常会有一类项目,这类项目很大、很重,
涉及的业务内容多而杂,还涉及了跨部门共同维护,积累的庞大的技术债 等各种问题。
这类项目在维护成本上、部署成本上等,都会花费巨大的开销,前端开发人员对于维护这类项目,苦不堪言,
急需找到解决这类问题的方案。
基于这样的背景下,开始探讨 解决方案的可行性, 微前端 正是基于此 开始慢慢 出现在人们的视野中。
## 现状
### 发展历程
在 Web 的发展初期,还没有所谓的前端的概念,网页的内容也相对简单,大多仅涉及文字图片信息的展示和表单内容,
这些工作可能网站负责人自己就包办了。
然后微软推出了 **Ajax** 技术,引起了网页的技术变革,从此网站开始具备了动态交互性,
能够在网页发起请求动态获取服务器的内容这丰富了网页的可交互性网页的开发也从UI界面和表单交互进一步增加了
数据和逻辑的开发,前端也慢慢的被划分一个相对独立的职能。
而伴随着 nodejs 的出现,以及 angular 的出现,还包括 vue/react 等库,以及建立在 nodejs 上的生态,
包括grunt、gulp、webpack 等工具的诞生,前端进入了一个喷井式爆发的时期,也是我们所处的时期。前端越来越专业化,
包含的技术内容越来越丰富依托于nodejs 以及众多的技术框架等,向着工程化进一步的发展,前端项目也越来越大。
### 浮现的问题
你是否维护过一个可能有着四五年以上历史的项目?是否维护过一个糅杂了各种各样的库的项目?
是否维护过一个多个公司部门参与的跨团队联合开发的项目?
对于很多人来说,入职的某个公司,最怕被安排去维护一个这样的项目。因为每一次维护迭代,就如同开盲盒一样,
永远不知道有什么惊喜在等着自己。
对于这类项目,可能存在的问题包括:
- 跨部门,夸团队联合开发,如何沟通?如何协作?
- 业务线多,页面多,如何管理?
- 如何进行代码管理?
- 如何进行分支管理?
- 多部门进行迭代,如何把控版本?
- 存在发布冲突,如何解决?
- 如何进行测试?
- 如何管理公共代码?
- ...
可能改动某一行代码,都会带来意想不到的结果,种种问题的积累,技术债的、业务债的,使得项目越来越臃肿,越来越难以维护。
亟需寻找一种方案,能够解决这些问题。
### iframe嵌入
于是,在大多数时候,我们不得不去选择通过 iframe嵌入 的方式,先把臃肿的项目一点一点的拆开给回各个部门或者团队自行维护,
再提供一个 系统门户应用,用 iframe嵌入 的方式,加上维护一个菜单列表与其他项目入口链接的映射,来糅合成一个 网站应用。
![iframe嵌入](//assets.processon.com/chart_image/630f92445653bb0c5d1040ab.png)
通过 iframe嵌入在一定程度上满足了 各部门各团队各业务线 独立开发独立部署的需求,只需要提供对应的页面链接就可以接入到
整个系统中。但也存在着一些问题
**安全问题**
然而,我们知道, iframe是存在安全问题的如果攻击者使用 iframe访问一个 未知来源的链接,有可能被注入恶意脚本,从而盗取
系统的隐私信息,这需要我们去严格配置 SCP以及配置 sandbox尽可能的保证 iframe 的安全性。
**资源加载过多**
而由于仅需要提供链接就可以嵌入,那么对于各自的项目来说,灵活度就很高,各个项目可以随意的选择各种技术框架来实现自己的业务,
又或者即使使用了相同的技术框架,但各项目的资源相对独立,对于整个系统而言,需要加载的资源量会十分庞大,
从而导致了页面访问速度变慢,经常会出现页面部分区域白屏等待时间过长等,这也带来了体验问题。
**访问地图丢失**
由于 iframe 嵌入的站点,独享访问历史记录,与外部的历史记录是相互独立的,即通过浏览器的 前进/回退 按钮来访问历史记录并
不能得到预期的结果,这在一定程度上影响了用户的操作。
## 寻找解决方案
有没有什么其他的方案,来进一步解决这些问题呢?
首先我们明确的知道,单项目管理目前来看不是一个可行的方案,需要在多项目管理上寻求解决方案。
### 多项目公共业务组件
对于多数的大型系统项目而言,大体上都采用以下布局结构:
![微前端-2](//assets.processon.com/chart_image/6311781d079129320755bec5.png)
主体布局结构包括:
- 导航栏 (可选)
- 左侧菜单栏 (可选)
- 内容区域
- 页脚 (可选)
在这种布局结构下,各个业务板块通常通过 导航栏 或者 左侧菜单栏 进行 导航切换,在 内容区域 展示 业务板块。
即,总体上看,对于业务来说,导航栏、左侧菜单栏、页脚,这几个都是可能 共同的,主要的不同点在于 内容区域。
那么我们可以把 共同的部分,如 导航栏、左侧菜单栏、页脚 这几个部分,抽离为公共业务组件,
对于每个业务板块,独立为单独的项目进行开发维护,并在项目中引入这些 公共业务组件。
公共业务组件其中主要负责之一是提供 链接到各个业务板块。
![微前端-3](//assets.processon.com/chart_image/631301e65653bb40f833b613.png)
这种方案具有如下的优点:
- 整体系统根据业务板块拆分为了多个项目;
- 实现了项目的独立性,可独立进行开发、发布;
- 通过在主项目重载渲染,实现类似 **SPA应用** 的访问体验;
但同样也带来了新的问题:
- 公共业务组件
- 公共业务组件 如何进行管理;
- 公共业务组件 如何在业务板块项目之间保持同步更新;
以及没有解决的问题:
- 资源加载过多;
 各个业务板块项目重复加载公共业务组件,重复加载各种库资源。
- 项目无法实现统一管理;
### 主项目重载业务项目资源
在上一个方案中,公共业务组件的引入解决了一部分问题,也带来了一部分问题,如何把公共业务组件进行统一管理,并保持一致性?
我们回到 iframe方案在这个 iframe方案中有一个主项目用于管理这些 菜单栏、导航栏等。 同样的,可以借鉴这个思路,
也抽象一个主项目,用于管理这些 公共业务组件,然后寻找另一种方式来加载渲染其他的业务板块项目。
我们知道,业务板块的项目,也是通过链接去访问的,而每个链接都对应着一个 html 资源文件通过加载这个资源以及HTML内的 css资源、js资源等来渲染页面。那么我们可以通过解析这个 html资源然后将得到的 html内容、css文件、js文件在主项目中加载后渲染到特定的区域
那么就可以做到在主项目中加载业务板块项目。
![微前端-4](//assets.processon.com/chart_image/6314ea9cf346fb55d89d4e77.png)
在主项目中,实现一个 资源加载器与解析器,通过业务板块项目的访问链接,获取 html资源文件并解析 html 的内容,包括:
- `<head>` 标签中的 `<title>`, `<link>`, `<script>` 等;
- `<body>` 标签中的 html 内容,`<script>`
然后加载 解析得到的 CSS资源、JS资源将 html内容 插入到 特定的区域中,并进行渲染。
从而呈现完整的网页内容。
这种方案,进一步解决了如下的问题:
- 公共业务组件交由 主项目进行统一管理,直接避免了同步问题;
- 业务板块均在主项目中渲染,提高了用户体验;
然而,也引入了新的问题:
- 业务板块都运行在同一个环境中,多个板块之间切换,加载的资源容易对环境产生污染,
如污染了某个全局变量、polyfill相互污染等。
- 可能存在 加载资源跨域问题。
但是也拥有了如下的优点:
- 拆分项目,可独立开发和部署;
- 主项目统一管理 公共业务组件,更易于维护;
- 项目间的切换得到体验优化;
当方案思考到了这里,发现,主项目是通过 解析 **链接** 来加载业务板块项目,
**链接** 对于现代前端来说,更多的意义是可能是 **路由**。那么我们顺着这个思路,继续优化,
## 新的方案
说起路由,我们很容易想到,像如今的 `react`, `vue`, `angular` 等主流的库/框架, 通过 路由 来实现 `SPA` 应用,
或者说, 通过 **路由分发页面**
那么,我们可以进一步的扩展这个思路,是否可以通过 **路由分发应用**
### 路由
在前端的范畴中,路由指 随着浏览器的地址栏变化,而呈现不同的内容给用户。
通常使用 **hash** 或者 **history API** 实现前端路由。
``` js
// hash
`https://pengzhanbo.com/#route`
// history API
`https://pengzhanbo.com/route`
```
路由进一步细化,通过 `/` ,又可以 划分为 一级路由、二级路由、三级路由... 等多级路由。
在现代的前端框架如 `React` / `Vue` / `Angular` 等,均有通过 路由 实现 SPA应用 的技术方案。
而 SPA应用 就是 **路由分发页面**
### 路由分发应用
**路由分发页面** 类似,我们也可以通过 **路由分发应用**
类似于 主项目重载业务项目资源,通过 实现 路由与业务板块项目的映射关系,
在主项目中通过路由寻找业务板块项目,加载相关资源并渲染在相关区域。
### 主应用与子应用
从这里开始, 我们将 主项目 定位为 主应用, 将各个 业务板块项目 定义为 子应用。
在主应用中实现 子应用加载器,加载器通过 解析路由来获取加载对应的子应用。
**主应用:** 作为独立的项目,整个系统的入口应用,负责统一管理公共业务组件(如 菜单栏、导航栏、页脚等),负责实现子应用加载器,负责实现渲染子应用的容器。
**子应用:** 作为独立的项目,系统的各个业务板块分别独立为单独的项目,单独开发维护与部署。
### 注册子应用
主应用需要通过路由发现子应用,需要建议起 路由与子应用的映射关系,所以需要有一套机制,用于向主应用注册子应用,
并关联相关资源文件等。
``` json
[
{
"AppName": "Sub Application",
"route": "/sub-app-route",
"resource": {
"js": [
"https://example.com/index.js"
],
"css": [
"https://example.com/style.css"
]
}
},
// more ...
]
```
## 微前端
通过将整个系统拆分为一个个小型的项目,小型项目即为子应用,通过细化,将整个系统细化为一个个微小的应用,
从而实现了降低整个系统的复杂性。
一个小型项目可以是某个部门的业务项目,可以是某个业务项目中的某个板块,也可以是一个单独的页面。
这也是为什么将新的方案称之为 **微前端**
微前端是指,通过将一个系统,拆分为一个个 微小的独立的子应用,通过主应用聚合为一个完整的系统。
微前端是一个与框架无强关联的概念,可以类比于服务端的微服务,是浏览器端的微服务。
![](//assets.processon.com/chart_image/6318b7297d9c0833ec81b2cd.png)
由于子应用是独立的,理论上是支持使用任意的技术框架进行开发,无论是使用 jQuery开发还是使用 Vue、React、Angular等。
然而在实际中,对于整个系统而言,技术框架的选择应该保持统一性,以保证整个系统的可维护性。
## 微前端的局限性
微前端的技术方案,更适合于 中大型的项目中使用,而对于小型项目而言,由于本身体量不大,没有必要对整个系统进行进一步的细化,
细化反而增加了项目的复杂度。
而对于中大型项目而言,如果是老系统迁移到微前端的方案,那么不可避免的,还需考虑新旧方案之间的迁移过渡的方案以及规划。
如果老系统中存在应用了多种不同的技术框架,或者同框架的不同版本,由于主应用、所有子应用均运行在同一个浏览器环境中,
不可避免的存在环境污染问题如全局环境污染polyfill对于原生对象的多次污染等还包括CSS的命名污染等问题。
所以如何保证子应用的正确渲染,如何避免环境污染问题,也是亟需解决的问题。
## 微前端的未来
目前来看,微前端主要分为 主应用 和 子应用,在 **微** 上,也仅细化到页面级别,然而,对于微前端而已,还可以进一步的细化,
如,细化到页面的某一个区块,细化到某一个逻辑功能,均可以通过微前端的技术方案,共享到主应用以及子应用中使用。
整个系统愈加化整为零,将复杂度进一步的拆解,细化,令每一块功能、逻辑等都能使用通过某个项目提供,甚至独立的项目进行维护和部署。
微前端是一个与框架无关的概念,但在实现微前端时,如果允许多技术框架共存,所带来的问题的,反而比不使用微前端时所存在的问题,要更难以预料,难以解决。在实际的场景中,最好还是限定在统一的技术框架范畴中,避免由于共存不同的技术框架,而引入更为复杂的问题,
## 结语
微前端是一个相对新兴的技术概念,适用于一些前端场景,但最好是你已经考虑清楚了,微前端是解决你的场景问题的最好方案,否则,除非必要,
无需选择微前端方案。

View File

@ -1,60 +0,0 @@
---
title: 浅谈前端低代码
createTime: 2022/09/13 08:15:38
author: pengzhanbo
permalink: /article/ecdgrife/
article: false
---
前端低代码在最近的这两年,不少的公司或技术团队都对此青睐有佳,并各自实现了各自的低代码平台。
<!-- more -->
## 前言
前端低代码,是指 无需代码或者仅需少量的代码,即可生成可交互的应用。
这个概念的兴起,期望于能够更快的去构建、部署新的应用,并降低门槛,让非技术开发人员也能够构建新的应用。
## 为什么做低代码
传统的应用开发从启动到发布的过程,大致的流程如下:
::: center
![low-code-1](//assets.processon.com/chart_image/6320a2fb637689341d579d34.png)
:::
在这个过程中,我们需要花费大量的时间用于 代码开发 -> 测试 这个过程,在这个过程,还需要根据项目大小,组织多个开发人员、测试人员等参与到项目中,包括制定开发规范、测试规范等。
而对于某些场景的应用,可能整个应用的生命周期相对较短,多个应用之间存在着类似的功能、需求、设计等等,然而在传统的项目开发中,
我们仍然需要按照上述的流程完整的走一遍才能正式发布上线这无疑会花费大量的时间。一般我们会通过抽离重复的功能、需求、UI等为
独立的库、组件等,在新项目中实现复用,从而减少开发时间,然而这并不能对项目的发布速度有质的提升。
而对于一些小型企业,或者个体经营户,期望做一个线上应用,但并没有多余的资金资源去组建一个开发团队,对购买服务器、上线应用等更是一知半解,成为了制约他们发展的一道坎。
对于这些场景、存在的问题,需要需要一种方案,能够实现快速的实现从创建项目到发布部署为可访问的项目,并且能够面向更广泛的用户群体。
这成为了一个非常具有市场潜力的需求。
## 如何做低代码
对于一个前端应用,通常由多个页面组成,在现代前端开发中,我们将页面拆分为一个个组件来进行组合:
![](//assets.processon.com/chart_image/6320ccbf1efad46b0aa9d631.png)
在 前端低代码 中,我们同样的,可以通过 组件来组装页面,通过可视化的交互方式,将组件拖拽到 页面容器中,
这种交互方式相对来说更加适用于更多的群体。
![](//assets.processon.com/chart_image/6320d1830e3e743f58315ed7.png)
同时,需要提供能够对组件进行编辑状态的能力,以支持应用的个性化配置。
![](//assets.processon.com/chart_image/6321b4420e3e743f5833bbc8.png)
在初步确定好 基础的功能、交互方式后,就可以围绕它们,来完善 实现低代码平台的技术方案。
----
初步明确的,我们需要 通过 **组件** 来组装 应用,围绕这一块,需要实现:
- 低代码组件的规范:开发规范、接入规范;
- 用于承载组件、组装组件并渲染的应用容器;
- 组件的状态的更新与保存;

View File

@ -1,109 +0,0 @@
---
title: 移动端适配方案
createTime: 2020/08/14 01:54:29
permalink: /article/vhpmovsm/
author: pengzhanbo
tags:
- develop
---
## 背景
移动端设备由于不同品牌、不同机型,不同设备中,使用的不同浏览器,带来的一系列适配问题。在这些设备中,如何实现展示效果、交互的一致性,是比较头疼的问题。
## 发展
早期的适配方案五花八门。
在2015年双十一左右 阿里前端团队 AmFe 分享了 `flexiable` 的移动端适配方案,在往后的几年中, `flexiable`成为了主流的移动端适配方案在各大移动端应用中使用。
随着技术的发展CSS3的`viewport`越来越得到了更多的设备支持, 逐渐的可以直接使用 viewport 来作为 移动端适配方案2017年左右开始步入开发者的视野。
后来 AmFe 宣布 推荐使用 `viewport` 方案代替 `flexible``viewport`方案逐渐成为主流适配方案。
## lib-flexible 方案介绍
### viewport
viewport 即浏览器窗口在移动端设备中viewport太窄为了更好的服务于CSS提供了 `visual viewport``layout viewport`
### 物理像素physical pixel
物理像素即设备像素,是显示设备中最微小的物理部件。
### 设备独立像素density-independent pixel
设备独立像素也称为 密度无关像素,可以认为是计算机坐标系统中的一个点,这个点代表一个可以由程序使用的虚拟像素,然后由系统转换为物理像素
### CSS像素CSS pixel
css像素是一个抽象单位主要用在浏览器上用来精确的度量Web页面上的内容。
一般情况下, CSS像素称为设备无关的像素简称 `DIPs`
### 屏幕密度
屏幕密度是指一个设备表面上存在的像素密度,它通常已每英寸有多少像素来计算(`PPI`
### 设备像素比device pixel ratio
简称 `DPR`,定义了物理像素和设备独立像素的对应关系
```html
设备像素比 = 物理像素 / 设备独立像素
```
### 简要说明
`flexiable` 通过hack手段根据设备的dpr值相应改变 `<meta>` 标签中viewport的值
```html
<!-- dpr = 1-->
<meta name="viewport" content="initial-scale=scale,maximum-scale=scale,minimum-scale=scale,user-scalable=no">
<!-- dpr = 2-->
<meta name="viewport" content="initial-scale=0.5,maximum-scale=0.5,minimum-scale=0.5,user-scalable=no">
<!-- dpr = 3-->
<meta name="viewport" content="initial-scale=0.3333333333,maximum-scale=0.3333333333,minimum-scale=0.3333333333,user-scalable=no">
```
从而让页面达到缩放的效果,变相的实现页面的适配功能。
主要的思想:
1. 根据 `dpr`的值来修改 `viewport` 实现1px的线
2. 根据 `dpr`的值来修改 `html``font-size`从而使用rem实现等比缩放
3. 使用 `hack` 手段用`rem`模拟 `vw`的特性
### 使用
> github: [https://github.com/amfe/lib-flexible](https://github.com/amfe/lib-flexible)
>
> px-to-rem: [https://www.npmjs.com/package/postcss-pxtorem](https://www.npmjs.com/package/postcss-pxtorem)
>
## px-to-viewport 适配方案
`Flexiable` 是通过javascript 模拟 `vw`的特性,到今天未知,`vw`已经得到了众多浏览器的支持,完全可以考虑直接将`vw`单位用于我们的适配布局中。
在css level3 中,定义了和 viewport相关的四个单位分别是 `vw``vh``vmin``vmax`
- `vw`: viewport width 简写1vw等于 `window.innerWidth``1%`
- `vh` viewport height简写1vh 等于 `window.innerHeight``1%`
- `vmin`vmin的值是当前 vw和vh中较小值
- `vmax` vmax的值是当前 vw和vh中较大值
![](/images/viewport.png)
在一张 750px的设计稿中 100vw=750px 1vw=7.5px通过公式即可转换px单位为vw单位实现适配。
可以通过 postcss-px-to-viewport 来帮助实现自动转换
> github: [https://github.com/evrone/postcss-px-to-viewport](https://github.com/evrone/postcss-px-to-viewport/blob/master/README_CN.md)
>
### 适用vw适配页面的场景
1. 容器适配
2. 文本适配
3. 大于1px的边框、圆角、阴影
4. 内边距和外边距

View File

@ -1,143 +0,0 @@
---
title: 表单配置化生成方案
createTime: 2022/03/17 10:10:04
author: pengzhanbo
permalink: /article/y979pp8x/
---
一个简单的基于 vue3 的表单配置化生成方案,可以作为思路参考。
<!-- more -->
## 前言
本方案是个人在做某个项目时,需要实现一个简单的表单配置化生成,而做的一个方案。
最终结果是通过一份表单配置,渲染为一个表单,并支持:
- 支持多种表单字段类型 (单行文本/多行文本/数字/下拉/多选/单选 等),并支持嵌套字段(对象/数组)
- 支持字段校验
- 支持条件判断显示/隐藏 表单字段
- 支持字段分组展示
::: tip
完整代码仓库地址: [formative](https://github.com/pengzhanbo/formative)
:::
## 效果展示
**通过配置生成表单:**
![](/images/formative-demo-1.png)
**配置的字段为对象或者数组时:**
![](/images/formative-demo-2.png)
**对配置的字段进行分组展示:**
![](/images/formative-demo-3.png)
## 方案说明
通过配置生成表单,需要明确如何实现 **表单字段** 与对应的表单组件进行关联,并实现**数据绑定**。
### 协议配置
约定一个协议,通过一个对象来表示 表单中某一个字段:
**FieldSchema:**
```json
{
"type": "表示字段的类型,比如单选、多选、单行文本、多行文本等",
"field": "表示字段的名",
"label": "表示字段显示的标签",
"default": "字段默认值",
"description": "字段描述信息,作为提示信息使用"
}
```
对于完整的表单,则使用 `FieldSchema[]` 的数组形式进行配置。
### 表单字段与组件关联
在 vue 中, 组件可以通过 `h(component, attrs[, children])` 渲染函数 进行渲染,
通过 `h()` 函数,可以实现动态渲染不同的组件。
我们可以利用这一特性,首先 字段类型与对应的组件进行映射,通过映射关系,用 字段类型获取对应的组件,
然后调用 `h()` 进行渲染。
``` ts
// 字段类型 与 组件 映射
const componentMap = {
radio: RadioField,
checkbox: CheckboxField,
text: TextField,
select: SelectField,
}
```
``` tsx
{
name: 'Field',
props: ['fieldType'],
setup(props) {
return () => h(componentMap[props.fieldType])
}
}
```
### 表单数据绑定
对于表单数据,需要实现 数据初始化,以及 对应的 Field组件对数据的可读写。
在vue中我们可以使用 `provide/inject` API 在 表单组件的根组件,通过 `provide()` 进行注入,在内部的
`FieldComponent` 中通过 `inject()` 获取对应的数据,进行读写。
``` ts
/**
* 通过 provide 输入数据
*/
const useFormDataProvide = (formData: FormData): FormInjectKey => {
const key: FormInjectKey = Symbol('formData')
provide(key, formData)
return key
}
/**
* 通过 inject 获取数据
*/
const useFormData = (key: FormInjectKey): FormData => inject<FormData>(key)!
```
由于需要支持 字段为对象/数组时产生的 多层级数据结构,还需要考虑 `FieldComponent``FormData.a.b`
深层数据的读写。
可以通过实现 `dotPath` 通过 `FormData['a.b']` 的形式来读写 `FormData.a.b`,这样 `FieldComponent`
只需要知道 `a.b``dotPath` 字段路径即可配合 `inject()` 获取的表单数据进行读写。
``` ts
const useDotPath = <T = any>(model: FormData, dotKey: ComputedRef<string>) => {
const binding = computed<T>({
set(data) {
setDotPath(model.value, dotKey.value, data)
},
get() {
return getDotPath(model.value, dotKey.value)
},
})
return binding
}
```
## 技术实现
在 [formative](https://github.com/pengzhanbo/formative) 仓库中, 通过本方案实现了一个 配置化生成表单的库。
- `src/components/Formative.tsx` 文件作为表单根组件。
- `src/components/Field.tsx` 文件作为根据表单字段动态选择对应组件进行表单字段渲染。
- `src/components/Group.tsx` 文件实现了对表单字段进行分组。
- `src/components/[other].tsx` 其他文件实现了 各种表单字段组件。
有兴趣了解细节的,可以阅读 [formative 源码](https://github.com/pengzhanbo/formative)
我在关键位置进行了相关的代码注释说明。
同时,你可以直接拉取 源码,在本地进行运行。并授权代码使用。

View File

@ -1,137 +0,0 @@
---
title: Jenkins 使用
lang: zh-CN
createTime: 2018/09/16 11:15:27
permalink: /article/bmtl5ah4/
author: pengzhanbo
tags:
- 工具
---
[Jenkins](https://jenkins.io/) 是一款功能强大的应用程序,允许持续集成和持续交付项目。这里记录一些 Jenkins 使用的方法。
<!-- more -->
_以下基于 `CentOS` 系统。_
### 安装
安装详见 官网 [Jenkins 安装](https://jenkins.io/download/) 流程,各个系统如何安装均有说明。
环境依赖: `java`
CentOS 下安装:
``` bash
sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat/jenkins.repo
sudo rpm --import https://pkg.jenkins.io/redhat/jenkins.io.key
yum install jenkins
```
- __默认安装目录__ : `/var/lib/jenkins`
- __默认日志目录__ `/var/log/jenkins`
- __默认缓存目录__ : `/var/cache/jenkins`
- __默认admin密码文件__ : `/var/lib/jenkins/secrets/initialAdminPassword`
- __配置文件__ : `/etc/sysconfig/jenkins`
### 运行
``` bash
# 启动 Jenkins
service jenkins start
# 重启 Jenkins
service jenkins restart
# 停止 Jenkins
service jenkins stop
```
默认运行在 `8080` 端口, 本机可通过 `localhost:8080` 访问。
### 卸载
``` bash
service jenkiins stop
yum clean all
yum remove jenkins
rm -rf /var/lib/jenkins
rm -rf /var/cache/jenkins
rm -rf /var/log/jenkins
```
### 修改端口
1. 打开`Jenkins` 配置文件
``` bash
vim /etc/sysconfig/jenkins
```
2. 修改 `$HTTP_PORT`
``` bash
$HTTP_PORT="8080"
```
### 获取root用户权限
1. 打开`Jenkins` 配置文件
``` bash
vim /etc/sysconfig/jenkins
```
2. 修改 `HTTP_PORT`
``` bash
$JENKINS_USER="root"
```
3. 修改`Jenkins` 相关目录权限
``` bash
chown -R root:root /var/lib/jenkins
chown -R root:root /var/log/jenkins
chown -R root:root /var/cache/jenkins
```
4. 重启`Jenkins`并验证
``` bash
service jenkins restart
ps -ef|grep jenkins
# 若显示为root用户则表示修改完成
```
### 开机自启
``` bash
chkconfig jenkins on
```
### 全局工具配置
全局工具配置可以 配置相关工具如`Maven``GIT`等工具的路径、或者安装新的不同版本的工具。
配置该设置需要获取 `admin`权限,进入`系统管理 > 全局工具配置`
如:配置全局 GIT
![](/images/jenkins_globalconfig.png)
### 用户管理以及用户权限
- 使用`admin`权限的账号,进入`系统管理 > 用户管理`, 可以添加/修改/删除 用户。
- 进入`系统管理 > 全局安全配置` 中,勾选 __启用安全__。访问控制选择 __Jenkins专有用户数据库__,使用 __项目矩阵授权策略__, 可以为每个用户分配全局权限。
- 进入项目配置中,权限 __启用项目安全__ 可以单独为该项目分配用户权限。 从而确保每个项目的安全性。
### Git Parameter
为项目添加 `git`分支/标签选择参数构建配置,从而方便通过不同分支构建项目。
项目配置:
![](https://wiki.jenkins-ci.org/download/attachments/58917601/image2018-9-20_22-0-7.png?version=1&modificationDate=1537473611000&api=v2)
参数化构建:
![](https://wiki.jenkins-ci.org/download/attachments/58917601/image2018-9-20_22-2-47.png?version=1&modificationDate=1537473769000&api=v2)
基础`pipeline`配置:
``` groovy
// Using git without checkout
pipeline {
agent any
parameters {
gitParameter branchFilter: 'origin/(.*)', defaultValue: 'master', name: 'BRANCH', type: 'PT_BRANCH'
}
stages {
stage('Example') {
steps {
git branch: "${params.BRANCH}", url: 'https://github.com/jenkinsci/git-parameter-plugin.git'
}
}
}
}
```
[阅读插件原文git-parameter](https://plugins.jenkins.io/git-parameter)
### 其他
相关工具以及项目配置,都只是小问题而已...

View File

@ -1,35 +0,0 @@
---
title: NPM Binary 镜像配置
createTime: 2021/03/15 08:54:32
author: pengzhanbo
tags:
- 工具
permalink: /article/hsgdhlah/
---
在 NPM 安装 Electron, Puppeteer 等包时,他们会通过 `postinstall` 脚本下载对应的二进制文件。因为一些不得而知的原因这个过程在某些网络下可能会很慢或不可用。你可以复制以下配置至 `.bashrc``.zshrc` 中,使用 [npmmirror.com](https://npmmirror.com) 提供的二进制镜像。
<!-- more -->
数据来源于 `binary-mirror-config`
```sh
# === NPM BINRAY CHINA ===
# https://github.com/cnpm/binary-mirror-config/blob/main/package.json#L48
export NODEJS_ORG_MIRROR="https://cdn.npmmirror.com/binaries/node"
export NVM_NODEJS_ORG_MIRROR="https://cdn.npmmirror.com/binaries/node"
export PHANTOMJS_CDNURL="https://cdn.npmmirror.com/binaries/phantomjs"
export CHROMEDRIVER_CDNURL="https://cdn.npmmirror.com/binaries/chromedriver"
export OPERADRIVER_CDNURL="https://cdn.npmmirror.com/binaries/operadriver"
export ELECTRON_MIRROR="https://cdn.npmmirror.com/binaries/electron/"
export ELECTRON_BUILDER_BINARIES_MIRROR="https://cdn.npmmirror.com/binaries/electron-builder-binaries/"
export SASS_BINARY_SITE="https://cdn.npmmirror.com/binaries/node-sass"
export SWC_BINARY_SITE="https://cdn.npmmirror.com/binaries/node-swc"
export NWJS_URLBASE="https://cdn.npmmirror.com/binaries/nwjs/v"
export PUPPETEER_DOWNLOAD_HOST="https://cdn.npmmirror.com/binaries"
export SENTRYCLI_CDNURL="https://cdn.npmmirror.com/binaries/sentry-cli"
export SAUCECTL_INSTALL_BINARY_MIRROR="https://cdn.npmmirror.com/binaries/saucectl"
export npm_config_sharp_binary_host="https://cdn.npmmirror.com/binaries/sharp"
export npm_config_sharp_libvips_binary_host="https://cdn.npmmirror.com/binaries/sharp-libvips"
export npm_config_robotjs_binary_host="https://cdn.npmmirror.com/binaries/robotj"
```

View File

@ -1,11 +0,0 @@
---
title: caniuse
createTime: 2020/11/01 06:41:12
permalink: /article/h4z91gyz/
author: pengzhanbo
---
### 工具
将caniuse 的feature 结果以图片或者iframe的形式嵌入到站点。
[https://caniuse.bitsofco.de/](https://caniuse.bitsofco.de/)

View File

@ -1,136 +0,0 @@
---
title: git工作流实践
createTime: 2022/04/18 12:26:46
author: pengzhanbo
permalink: /article/cjeo0sia/
tags:
- 工具
banner: https://assets.processon.com/chart_image/6251bfce1efad407891be6c8.png
---
这是一篇在我个人工作实践中,在我参与的,负责的项目、团队协作中,逐步调整,适合于一些实际场景的 git 工作流实践。
众所周知,在软件开发中, git 是目前使用最广泛的 软件版本管理工具,它足够高效,足够安全,也足够灵活,
对于团队协作和软件管理提供了很大的帮助。
而一个良好的合适的 分支管理方案,可以更好的帮助我们通过 git 进行 软件版本管理。
在过去, git-flow 是一个很流行的 分支管理方案。对于多人协作,中大型项目,提供了一个较为满意的解决方案。
但在我过去的实践中,逐渐感受到 git-flow 对于我来说还是太过于繁杂了,虽然在事实上满足了 我对于项目的软件版本管理,
但认为还是可以做一些简化,调整 分支管理策略,以更好的适配实际的开发工作。
## 场景
试想一下,在我们实际的软件开发过程中,迭代一个软件的版本,正常来说是:
```
需求评审 -> 需求确认 -> 开发阶段 ->
测试环境测试 -> 预生产发布验证 -> 生产环境发布验证 -> 发布完成
```
### 从开发到发布
在这个过程中,如果在 测试环境发现了 bug需要重新回归到 开发阶段进行修复,
同样的,在预生产发布验证环节发现了问题,也需要重新回归到 开发阶段修复后,重新测试和发布预生产。
而如果 生产环境发布验证 出现了问题, 需要紧急回滚到上一个版本,并重新回到开发阶段修复,测试,预生产验证,再生产发布。
### 需求明确 or 变更
虽然我们一直强调,版本迭代进入开发后,这个版本的需求应该是明确的。
然而,这仅仅只是理想情况下,实际过程中经常会遇到 需求变更,甚至是 添加新的功能,删除功能等。
这种变更,还很难控制会发生在开发阶段还是测试阶段。
也许我们会吐槽甚至抱怨这样的容易变更需求的项目管理很糟糕,很不专业。但是,这也是确确实实存在的场景,
即使再成熟的团队,也会不可避免的遇到这种问题。
然而需求变更也不代表着一定是上面这种场景。
### 周期性版本发布,需求池
有些项目可能相对来说比较成熟,或者采用了不同的版本迭代方式。
周期性版本发布指,采用某个固定的周期内,仅会也一定会发布一个版本,如 每两周发布一个版本。
那么在这种模式下,每个版本上线哪些功能,上线多少功能,实际上是以各个功能的开发进度,和进入到当前周期后,
明确下来的版本计划再决定的。
将产品 明确划分为一个个需求,建立一个需求池,并指定了每个需求的按时间的线性迭代预期计划,
在预期的未来的周期性的版本发布中,会上线哪些需求功能。
在这种场景下,我们不可能把需求都集中在某个分支上进行开发,仅有的一个 develop分支也很难满足所有的
开发合并和测试。
### 开发人员对于 git的掌握
很多时候,对于工具能够使用到什么程度,对于项目能够有多少帮助,其实不在于 工具有多优秀,而是在于团队成员
对于工具的平均掌握程度,已经是否有成熟的规范化的操作流程。
早期我认为 成员对于 git 的掌握是大差不差的,只要说明一些操作流程即可,未加以限制和规范
然而,这会也给项目带来了潜在的风险:主要分支可能会被误操作污染,多个功能直接在同一个分支开发等等。
而为了解决这些问题,需要建立一套足够完善,同时低复杂度的操作规范,并对人员和分支加以权限控制。
## 分支管理实践
在以上的场景中,逐渐摸索出了一套合适的分支管理方案:
- `master` 分支: 主分支,正式版本的代码提交记录。
永远不会在上面做任何改动提交,也仅接受来自 `release` 分支的合并请求合并后打上版本tag。
- `release` 分支:发布分支,用于发布版本到预生产和生产环境,发布完成后合并到 master分支。
永远不会咋上面做任何改动提交,也仅接受来自 prerelease 分支的合并请求。
- `prerelease/*` 分支组: 预发布分支,根据版本号创建新的分支。
仅合并当前版本关联的 `feature/*` 分支,用于合并开发完成的需求,进入到版本测试阶段,发布到测试环境。
不会或者尽可能少的在上面做任何改动提交。
- `feature/*` 分支组: 功能开发分支(包括hotfix),从 master 分支拉取创建的分支,
每个分支仅针对某一个单一的业务或者功能,所有的改动都应该在 feature 分支上进行。
仅能合并到 prerelease 分支。 master有更新后需要及时拉取合并同步。
流程图:
![git-work-flow](https://assets.processon.com/chart_image/6251bfce1efad407891be6c8.png)
可以看出,整个方案只有四条主要的分支,且不同功能分支到其他分支都是单向的。
这样的好处是足够的简单,足够的好理解,也保证了一定的灵活性。
删除了 develop分支的原因是单一的develop分支并不能很好的满足 需求变更后导致的 develop分支无效
选择使用 prerelease 分支组作为替代,可以灵活的根据版本规划创建分支,即使发生需求变更,也可以灵活的直接抛弃
当前已创建的分支,重新新建一条 prerelease分支再次从feature分支合并代码。
从 develop 改名为 prerelease 也是因为,功能分支开发完后并不是马上合并的,而是根据迭代计划再合并的,理解起来就是
预期上线的功能,即 prerelease。
从分支的行为上看, master分支和 release分支 似乎可以在简化仅保留一条。但这里是考虑到 如果有发布回滚操作,如果
都在 master 分支上进行那么master分支的操作记录看起来就不够的干净简洁而把发布、回滚等策略都放到了 release分支
,而 master仅保留干净的版本历史那么会更加的友好便于维护。
在这个方案中,最好是配合上 git分支的权限控制对 master分支、release分支进行保护确保这两个分支仅接受来自对应
分支的合并请求。
分支合并,选择 `merge` 还是 `rebase`,这个需要对 commit记录有何要求去考量。
如果需要保证所有提交记录都可追踪,建议使用 merge如果希望 提交记录线性、整洁,建议使用 rebase。
同时feature分支 合并到 prerelease 分支,最好是通过 pull requests 的操作模式,在进入测试阶段前,接受来自其他
有用审查权限的开发人员的code review。
::: warning 说明
该实践方案仅是我过去在我的工作实践中总结的并在团队内部经历过超过2年的验证和调整所得出的适合于当时团队的实践方案。
是否与您的团队或所在的团队契合,还需要重新考量。
分支管理方案 需要从实际的情况触发考虑,包括团队人数,产品规模等等。
没有最好的方案,只有合适自己的方案。
:::

View File

@ -1,128 +0,0 @@
---
title: VSCode 常用插件推荐
lang: zh-CN
createTime: 2018/12/29 11:15:27
permalink: /article/ofp08jd8/
author: pengzhanbo
tags:
- VSCode
---
`VS Code` 作为我现在工作中最常用的编辑器,也是我十分喜欢的编辑器。它强大的功能和插件系统,对我的工作提供了很多帮助和支持。将我在工作中经常使用的插件,推荐给大家。
<!-- more -->
### Code
1. [Code Spell Checker](https://github.com/Jason-Rev/vscode-spell-checker)
单词拼写检查插件,帮助检查代码中单词是否拼写错误,包括驼峰形式的变量名称检查。可以在一定程度避免一些不必要的单词拼写错误导致的一些低级错误。
2. [ESLint](https://github.com/Microsoft/vscode-eslint)
javascript ES6 代码规范、语法检查工具,帮助规范团队代码规范。
3. [EditorConfig](https://github.com/editorconfig/editorconfig-vscode)
编辑器配置,代码格式规范相关,必备。
4. [Prettier](https://github.com/prettier/prettier-vscode)
帮助格式化`javascript``typescript``CSS`代码。 <br />
`Prettier` 会读取 `.editorconfig`,或根据提供相关配置,格式化代码为符合项目代码规范。
5. [Bracket Pair Colorizer](https://github.com/CoenraadS/BracketPair)
可以对每一个代码块或者每一层嵌套,以不同的颜色高亮,帮助阅读代码。
![Bracket Pair Colorizer](https://github.com/CoenraadS/BracketPair/raw/develop/images/example.png) <br/>
主要是针对 `()``[]``{}` 进行不同嵌套的颜色高亮
6. [Code Runner](https://github.com/formulahendry/vscode-code-runner)
`VSCode`中运行各种各样的语言。并将结果输出到输出控制台。
方便代码调试。
7. [Color Highlight](https://github.com/sergiirocks/vscode-ext-color-highlight)
颜色高亮插件,读取文件中的 十六进制、RGB、RGBA 等颜色,并以对应的颜色高亮显示。
### theme
1. [Atom One Dark Theme](https://github.com/akamud/vscode-theme-onedark)
一款 Atom 的 暗色系主题皮肤。 习惯了`Atom`编辑器,转而使用`VSCode`的小伙伴们可以使用这款皮肤。<br/>
颜色对比度适中,不会太过强烈。
![Atom One Dark Theme](https://raw.githubusercontent.com/akamud/vscode-theme-onedark/master/screenshots/preview.png)
### GIT
1. [Git Blame](https://marketplace.visualstudio.com/items?itemName=waderyan.gitblame)
可以帮助查看到文件的每一行的详细修改信息,包括 HASH串、作者、日期等。
2. [Git history](https://marketplace.visualstudio.com/items?itemName=donjayamanne.githistory)
以可视化的界面查看 git log 信息。支持:<br/>
查看所有分支或者某一个分支的log信息<br/>
查看某一个文件的log信息<br/>
查看某一个作者的log信息等。<br/>
3. [Git Project Manager](https://github.com/felipecaputo/git-project-manager)
该插件可以帮助你快速在VSCode新窗口打开本地git项目。<br />
命令:`ctrl+shift+p` / `cmd+shift+p` <br/>
或者按下 `F1`,输入 `GPM`
4. [Git Tags](https://github.com/leftstick/vscode-git-tags)
Git Tag 管理插件
5. [Git Lens](https://gitlens.amod.io/)
这款插件十分适合在多人协作项目中使用可以定位到当前文件每一行的最后提交作者、时间等git log也可以查看到当前文件的所有日志等。
如果有装这一款插件,`Git Blame`插件就没有必要装了。
6. [git ignore](https://github.com/CodeZombieCH/vscode-gitignore)
### Markdown
1. [Markdown Preview Enhanced](https://shd101wyy.github.io/markdown-preview-enhanced)
一款功能强大的 markdown 插件。让你在vscode 中拥有更好的 markdown 写作体验。
### Icon
1. [vscode-icons](https://github.com/vscode-icons/vscode-icons)
文件菜单 icons。 根据文件夹命名、文件后缀等,对文件夹、文件菜单栏添加 对应的`icon`
![vscode-icons](https://raw.githubusercontent.com/vscode-icons/vscode-icons/master/images/screenshot.gif)
2. [VSCode Great Icons](https://marketplace.visualstudio.com/items?itemName=emmanuelbeziat.vscode-great-icons)
另一款 文件菜单栏 icons支持 100+的文件类型。<br/>
![VSCode Great Icons](https://raw.githubusercontent.com/EmmanuelBeziat/vscode-great-icons/icons-test/icons.jpg)
### IDE support
1. [View In Browser](https://github.com/hellopao/view-in-browser)
快速打开html页面在浏览器中访问。
2. [ftp-simple](https://github.com/humy2833/FTP-Simple)
FTP 上传/下载插件。
3. [Rest Client](https://github.com/Huachao/vscode-restclient)
允许你在 `VSCode` 中发送HTTP请求并查看response的内容。
### Framework support
1. [Vetur](https://github.com/vuejs/vetur)
Vue 支持。
2. [minapp](https://github.com/wx-minapp/minapp-vscode)
微信小程序 支持。

View File

@ -1,36 +0,0 @@
---
title: 小徽章制作
createTime: 2021/07/20 04:37:25
author: pengzhanbo
permalink: /article/qrfeqp1q/
---
用于制作 小徽章的在线网站 或项目
- [https://shields.io/](https://shields.io/)
- [https://badgen.net/](https://badgen.net/)
- [https://forthebadge.com/](https://forthebadge.com/)
- [https://badge.fury.io/](https://badge.fury.io/)
- [https://github.com/boennemann/badges](https://github.com/boennemann/badges)
## example
``` md
![npm version](https://badge.fury.io/js/@vuepress-plume%2Fvuepress-theme-plume.svg)
![npm version](https://badge.fury.io/js/@vuepress-plume%2Fvuepress-theme-plume.svg)
![github version](https://badge.fury.io/gh/pengzhanbo%2Fvuepress-theme-plume.svg)
![release](https://badgen.net/github/release/pengzhanbo/vuepress-theme-plume/)
![npm](https://img.shields.io/npm/dw/@vuepress-plume/vuepress-theme-plume?style=plastic)
```
![npm version](https://badge.fury.io/js/@vuepress-plume%2Fvuepress-theme-plume.svg)
![github version](https://badge.fury.io/gh/pengzhanbo%2Fvuepress-theme-plume.svg)
![release](https://badgen.net/github/release/pengzhanbo/vuepress-theme-plume/)
![npm](https://img.shields.io/npm/dw/@vuepress-plume/vuepress-theme-plume?style=plastic)

View File

@ -1,108 +0,0 @@
---
title: 有用的工具列表
createTime: 2021/02/10 09:55:09
author: pengzhanbo
permalink: /article/xb4woxjg/
sticky: 10
---
## 浏览器插件
- [Vue.js devtools](https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd?) 顾名思义,非常有用的浏览器 vue开发调试插件
- [React Developer Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi) React 开发调试插件
- [Octotree - GitHub code tree](https://chrome.google.com/webstore/detail/octotree-github-code-tree/bkhaagjahfmjljalopjnoealnfndnagc) 对github仓库的文件tree生成侧边栏方便在线查阅浏览/跳转
- [XPath Helper](https://chrome.google.com/webstore/detail/xpath-helper/hgimnogjllphhhkhlmebbmlgjoejdpjl) 提取当前网站选中的 DOM 元素的 XPath做埋点测试之类时有用
## cli开发
- [commander](https://www.npmjs.com/package/commander) 完整的命令行解决方案
- [cac](https://www.npmjs.com/package/cac) 轻量级的用于构建cli工具的解决方案
----
- [minimist](https://www.npmjs.com/package/minimist) 命令行参数解析工具
- [yargs](https://www.npmjs.com/package/yargs) 命令行参数解析工具
----
- [Inquirer](https://www.npmjs.com/package/inquirer) 交互式命令行工具
- [prompt](https://www.npmjs.com/package/prompt) 命令行 对话工具
----
- [shelljs](https://www.npmjs.com/package/shelljs) shell调用工具
- [execa](https://www.npmjs.com/package/execa) shell 调用工具
- [chalk](https://www.npmjs.com/package/chalk) node终端输出美化工具
- [ora](https://www.npmjs.com/package/ora) 终端 loading 工具
- [chokidar](https://www.npmjs.com/package/chokidar) 文件监听工具
- [is-ci](https://www.npmjs.com/package/is-ci) 检查当前环境是否是集成环境
## Http server
- [express](http://expressjs.com/)
- [connect](https://github.com/senchalabs/connect)
- [koa](https://koajs.com/)
- [fastify](https://www.fastify.io/)
## Server Framework
- [Nestjs](https://nestjs.com/) 类Spring boot的 Node端开发框架
- [Next.js](https://nextjs.org/) React 应用开发框架
- [Nuxt.js](https://nuxtjs.org/) Vue 应用开发框架
- [Think.js](https://thinkjs.org/) Node端开发框架
- [Egg.js](https://www.eggjs.org/index) 阿里开源的Node端开发框架
## VSCode 插件
### 皮肤
- [One Dark Pro](https://marketplace.visualstudio.com/items?itemName=zhuangtongfa.Material-theme) 个人觉得非常耐看,好用,舒服的一个皮肤。
- [Material Icon Theme](https://marketplace.visualstudio.com/items?itemName=PKief.material-icon-theme) 非常全面又好看的文件图标主题
### 辅助开发
- [IntelliCode](https://marketplace.visualstudio.com/items?itemName=VisualStudioExptTeam.vscodeintellicode) 辅助代码提示
- [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker) 拼写检查插件
- [Error Lens](https://marketplace.visualstudio.com/items?itemName=usernamehw.errorlens) 用更友好更直观的方式,将错误信息显示出来
- [Better Comments](https://marketplace.visualstudio.com/items?itemName=aaron-bond.better-comments) 更加友好的注释高亮
- [Bracket Pair Colorizer](https://marketplace.visualstudio.com/items?itemName=CoenraadS.bracket-pair-colorizer) 代码缩进高亮
- [Import Cost](https://marketplace.visualstudio.com/items?itemName=wix.vscode-import-cost) 显示包体积
- [Todo Tree](https://marketplace.visualstudio.com/items?itemName=Gruntfuggly.todo-tree) TODO注释高亮
- [indent-rainbow](https://marketplace.visualstudio.com/items?itemName=oderwat.indent-rainbow) 缩进高亮标识
- [Path Intellisense](https://marketplace.visualstudio.com/items?itemName=christian-kohler.path-intellisense) 文件路径提示
- [TypeScript Hero](https://marketplace.visualstudio.com/items?itemName=rbbit.typescript-hero) typescript 辅助开发工具
---
- [any-rule](https://marketplace.visualstudio.com/items?itemName=russell.any-rule) 正则表达式大全
---
- [Rest Client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) 类 Postman 的 Rest工具
## 测试工具
### assertion
- [chai](https://www.npmjs.com/package/chai)
- [should](https://www.npmjs.com/package/should)
### unit
- [jest](https://www.npmjs.com/package/jest)
- [ava](https://www.npmjs.com/package/ava)
- [mocha](https://www.npmjs.com/package/mocha)
- [karma](https://www.npmjs.com/package/karma)
- [tape](https://www.npmjs.com/package/tape)
- [@vue/test-utils](https://www.npmjs.com/package/@vue/test-utils)
### 2e2
- [nightwatch](https://www.npmjs.com/package/nightwatch)
- [cypress](https://www.npmjs.com/package/cypress)
## GIT相关
- [@commitlint/cli](https://www.npmjs.com/package/@commitlint/cli)
- [@commitlint/config-conventional](https://www.npmjs.com/package/@commitlint/config-conventional)
- [commitizen](https://www.npmjs.com/package/commitizen)
- [conventional-changelog-cli](https://www.npmjs.com/package/conventional-changelog-cli)
- [cz-conventional-changelog](https://www.npmjs.com/package/cz-conventional-changelog)
- [husky](https://www.npmjs.com/package/husky)
- [lint-staged](https://www.npmjs.com/package/lint-staged)
## 其他
- [Slidev](https://github.com/slidevjs/slidev) 为开发者打造的演示文稿工具
- [loupe](http://latentflip.com/loupe/) 代码执行流程可视化

View File

@ -1,200 +0,0 @@
---
title: Vue组件间通信
lang: zh-CN
tags:
- vue
createTime: 2018/07/20 11:15:27
permalink: /article/iezlvhvg/
author: pengzhanbo
---
在我们在进行基于[Vue](https://cn.vuejs.org/)的项目开发时,组件间的数据通信,是我们必须考虑的。
<!-- more -->
> 注: 本文所实现的方式,是在不考虑`vuex`下所做的实现。
我把组件间的关系,大致分为三种:
1. 父子组件
``` html
<parent>
<child></child>
</parent>
```
拥有类似结构,`parent`组件包含`child`组件,则`child`组件是`parent`的子组件,`parent`组件是`child`组件的父组件。
2. 兄弟组件
``` html
<item></item>
<item></item>
```
两个`item`组件在结构上同级,我们称之互为兄弟组件。
3. 跨多级组件
``` html
<list>
<item>
<message><message>
</item>
</list>
<dialog>
<content></content>
</dialog>
```
在这个结构中,`<list>``<message>`并不是直接的父子组件,中间还跨了一个级,在实际场景中,还会有跨更多层级的组件关系。`<message>``<content>` 组件两个既不是兄弟组件,又不是父子组件,而是跨了兄弟,父子的多级关系,实际场景中也会有发生交互。
那么这三种关系的组件,我们应该如何进行组件通信?
### 父子组件通信
要讲父子组件的通信,首先,我们需要了解 `vue` 组件的 特性。
1. 单向数据流,数据自上而下。
> Prop 是单向绑定的:当父组件的属性变化时,将传导给子组件,但是反过来不会。这是为
>了防止子组件无意间修改了父组件的状态,来避免应用的数据流变得难以理解。
2. 事件自下而上。
组件内部状态的变化,通过事件往上冒泡,通知上一级组件,由上一级组件监听事件,并触发相应回调。
基于以上,父子组件通信推荐的方式是:
父组件通过`props`将状态传到子组件,子组件通过事件将状态冒泡到父组件,由父组件监听触发回调改变状态。
`parent.vue`
``` html
<template>
<div class="parent">
<child
:name="name"
@name-change="nameChange"
>
</child>
</div>
</template>
<script>
import Child from './child';
export default {
name: 'parent',
data () {
return {
name: 'Jack'
};
}
methods: {
nameChange(name) {
this.name = name;
}
},
components: {
Child
}
}
</script>
```
`child.vue`
``` html
<template>
<div class="child">
<span>{{name}}</span>
<button @click="onClick">change name</button>
</div>
</template>
<script>
export default {
name: 'child',
props: {
name: {
type: String,
defualt() {
return '';
}
}
},
methods: {
onClick() {
this.$emit('name-change', 'John');
}
}
}
</script>
```
在某些例子或个人项目中,经常有发现到在子组件中使用 `this.$parent` 直接改变父组件的状态,诚然这种方式能够简化两个深耦合的组件的数据通信,在一些简单的场景中也会比较方便,但其实并不推荐采用这种方式实现父子组件通信,这样做的后果就是导致了数据流的不明确性,牺牲了单项数据流的简洁性,数据的变化流动变得不易于理解。
### 兄弟组件通信、跨多级组件通信
这两种组件关系,并没有直接的联系。
如兄弟组件,我们会很自然的想到使用他们的父级组件作为中转,将 `子组件1` 的状态通信到父组件,再由父组件通过 `props` 流向 `子组件2` ,反之亦然,但是如果兄弟组件间的交互复杂,但又与父组件没有存在直接的交互关联,父组件在这个过程当中,承担了多余的职责。
又如跨多级组件,上述例子中,`<list>``<message>`之间间隔了多层,如果我们继续使用父子组件通信`prop`和事件冒泡,中间的层需要重复的定义`prop`和事件,这显然也导致了它们承担了多余的职责。 `<message>``<content>` 组件之间,更是在结构上没有关联,`prop`和事件冒泡显得十分乏力,无法直接完成通信。
那么这两种组件关系,该如何完成通信,又不对它们中间层级组件,或者父级组件造成多余的干扰?
由于两种组件关系没有直接的关联,所以我们需要有一个桥梁,能够直接连接它们,使它们变得有关联。即,我们需要一个`中间件`
官方给我们的解决方案是`vuex`,但我认为它更多是的作为全局状态的管理,使用它作为某两个组件的通信中间件,显得大材小用,所以我这里不做讨论。
我所采取的方案是使用 自定义事件 完成组件通信。
__实例化Vue__
`vue`已实现了一套事件系统,可以很方便的使用它来完成我们的组件通信。
``` javascript
let middleware = new Vue();
export defualt middleware;
```
`message.vue`
``` javascript
export default {
name: 'message',
data () {
return {
info: 'hello'
};
},
methods: {
sayHello() {
middleware.$emit('say-hello', this.info);
}
}
};
```
`content.vue`
``` javascript
export default {
name: 'content',
data() {
return {
info: '';
}
},
created() {
middleware.$on('say-hello', info => {
this.info = info;
});
}
}
```
我们通过 `middleware``content.vue`注册了`say-hello`事件,当`message.vue`触发该事件时,`content.vue`监听到事件触发回调,从而实现了状态传导。
组件数据传导不再是通过`props`传导,而是通过事件进行通信。
__如果不使用实例化Vue的方式去完成我们也可以自己实现一套自定义事件。__ 这可以做更加个性化的自定义事件,满足项目中的多样的使用场景。
``` javascript
class Event{
constructor() {
// some props
}
on() {
// do something
}
emit() {
// do something
}
off() {
// do somethig
}
}
```
### 总结
复杂结构的组件通信,实现它们的通信,关键是实现中间件作为桥梁连接它们,无论是使用自定义事件,还是其他的方案。

View File

@ -1,211 +0,0 @@
---
title: 单仓库实现同时导出esm、cjs
createTime: 2022/04/06 08:33:04
author: pengzhanbo
permalink: /article/exports-esm-and-cjs/
---
在开发一些公共模块作为一个独立仓库时,有时候可能会在一个使用 es 的项目中通过 `import` 导入,
有可能在一个 cjs 项目中通过 `require` 导入。
如何实现单个仓库能够同时被 cjs 和 esm 项目导入呢?
<!-- more -->
## 为什么这么做?
在过去的时间里JavaScript 并没有一套标准的模块化系统,并且在过去的时间里,逐渐发展出了各种模块化解决方案,
其中最主流的有两种模块化方案:
- `CommonJs``cjs`,通过 `require('package')` 导出,`module.exports` 导入。
这套模块化系统应用与在`NodeJs``NPM packages`
```js
// in cjs
const _ = require('lodash')
console.log(`assignIn: ` _.assignIn({ b: '2'}, { a: '1' }))
// { a: '1', b: '2' }
```
- `Ecmascript modules`: 即`esm`在2015年`esm` 最终确定为标准模块化系统,浏览器以及各个社区开始逐渐
迁移并支持`esm`
```js
import { assignIn } from 'lodash'
console.log(`assignIn: ` assignIn({ b: '2'}, { a: '1' }))
// { a: '1', b: '2' }
```
`ESM`使用 `named exports`,能够更好的支持静态分析,对各种打包工具有利于做`tree-shaking`
而且浏览器原生支持作为一个标准代表的是JavaScript的未来。
同时,在`NodeJs``v12.22.0``v14.17.0`版本,开始实验性的支持`ESM`,并在`16.0.0`版本开始正式支持`ESM`
::: note
- ESM - [ECMAScrip modules](https://nodejs.org/api/esm.html)
- CJS - [CommonJs](https://nodejs.org/api/modules.html#modules-commonjs-modules)
:::
目前有很多包仅支持 `CJS` 或者 `ESM` 格式。 但同时,也有越来越多的包推荐并仅支持导出 `ESM` 格式。
但是相对来说,就目前而言,作为一个库,仅支持`ESM` 格式还是过于激进了。即使在 `NodeJs v16`已开始正式支持`ESM`
但是整个社区的迁移还是需要大量的时间成本和人力成本的,如果某个版本破坏性的从`CJS`支持迁移到`ESM`
那么可能导致一系列问题。
所以,如果一个库,能够同时支持`ESM`以及`CJS`,是一种相对来说更为安全的迁移方案。
## 共存问题
我们知道,`Nodejs` 能够很好的同时支持 `ESM``CJS` 进行工作,但是,有一个最主要的问题是,不能在一个 `CJS`
导入`ESM`,这时候会抛出一个错误:
```js
// cjs package
const pkg = require('esm-only-package')
```
```
Error [ERR_REQUIRE_ESM]: require() of ES Module esm-only-package not supported.
```
因为`ESM` 模块本质上是一个异步模块,所以不能用 `require()` 方法同步的导入一个异步的模块。
但是这并不意味着完全不能在 `CJS` 模块中使用`ESM` 模块,我们可以使用 动态 `import()` 的方式,来异步的导入`ESM` 模块。
`import()` 会返回一个 `Promise`
```js
// CJS
const { default: pkg } = await import ('esm-only-package')
```
但是,这并不是一个令人满意的解决方案,它与我们日常使用的模块导入方式来说,显得有点笨拙,不符合一般使用习惯,
我们还是更期望能够符合一般习惯的导入方式:
```js
// ESM
import { named } from 'esm-package'
import cjs from 'cjs-package'
```
## 如何做?
### package.json
在现在的稳定版本的`NodeJs` 中,已经支持同时在一个包中导出两种不同的格式。
`package.json` 文件中,有一个`exports` 字段,提供给我们有条件的导出不同格式。
``` js
{
"name": "package",
"exports": {
".": {
"require": "./index.js",
"import": "./index.mjs"
}
}
}
```
这一段声明描述了, 当进行导入包的默认模块时,如果是通过 `require('package')` 进行导入,那么引入的是 `./index.js` 文件,如果是通过`import pkg from 'package'`进行导入,那么引入的是 `./index.mjs` 文件。
`Nodejs` 会根据当前运行环境,选择合适的导入方式将包进行导入。
所以我们可以借助这一特性,来完成我们单仓库支持两个格式的第一步。
然后,下一个要解决的,就是如何构建两个格式的导出文件。
### Building
我们当然不可能为了同时支持`CJS``ESM`,而编写两份代码。
但我们可以借助一些构建打包工具,来生成`ESM``CJS`代码。
通常情况下,我们可能会使用 `rollup` 来构建打包我们的模块。
或者也可以使用 `tsup` 来构建。
#### rollup
当我们会选择 `rollup` 来构建一个库时,可能配置如下:
```js
// rollup.config.js
export default {
input: 'src/index.js',
output: {
file: './dist/index.js',
}
}
```
由于`rollup` 是支持多配置打包的,所以我们可以使用多配置的方式,同时打包输出两种格式的文件:
```js
// rollup.config.js
export default [
{
input: 'src/index.js',
output: {
file: './dist/index.js',
format: 'cjs',
}
},
{
input: 'src/index.js',
output: {
file: './dist/index.mjs',
format: 'es',
}
}
]
```
#### tsup
`tsup` 是一个面向 `TypeScript` 的打包工具,基于 `esbuild` 可以很方便的将我们的库打包成多种模式进行输出:
`tsup` 可以支持零配置,直接使用命令行即可输出两种格式
``` sh
tsup src/index.ts --format esm,cjs
```
执行完成后,将会得到两个文件:`cjs` 格式文件`dist/index.js``esm`格式文件`dist/index.mjs`
使用构建工具构建完成后,接下来就是完善 `package.json`
建议在使用 `type` 字段声明为 `module`, 来声明当前库时一个标准的 esm 库,以及添加 `main`,`module`,`exports`字段,
以便向下兼容:
```json
{
"name": "my-package",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"exports": {
".": {
"require": "./dist/index.js",
"import": "./dist/index.mjs"
}
},
"types": "./dist/index.d.ts",
"files": ["dist"]
}
```
最后,你的 `CJS` 项目中,或者 `ESM` 项目中,均可以根据环境要求,导入这个包。
```js
// cjs
const pkg = require('my-package')
```
```js
// esm
import pkg from 'my-package'
```
## 总结
虽然 `Nodejs``v14.22.0` 版本开始试验性的支持 `esm` ,并且到 `v16` 版本,正式支持 `esm`
但将库升级到仅支持`esm` 还是一个比较激进的做法,建议从相对安全的 双格式支持 开始迁移,在合适的时机,过渡到仅支持`esm`

View File

@ -1,149 +0,0 @@
---
title: 在NodeJs项目中使用ECMAScript module
createTime: 2022/06/17 02:04:57
author: pengzhanbo
permalink: /article/7jzjudus/
---
随着 `Nodejs v16` 成为长期稳定支持的版本,`ESM` 也随之成为 `NodeJs` 正式支持的标准化模块系统,这允许我们通过
`ESM` 来开发我们的 `NodeJs` 项目,并在项目中通过`ESM` 来导入其他的`ESM`包。
<!-- more -->
## 创建项目
我们以新建一个 NodeJs 项目为例, 它有如下的结构:
```sh
./my-esm-package
├── lib
│ ├── resolve.js
│ └── index.js
└── package.json
```
这个项目的功能是导出一个 resolve 方法,是 `path.resolve` 的封装实现。
::: code-tabs
@tab lib/index.js
```js
export * from './resolve.js'
```
@tab lib/resolve.js
```js
import path from 'path'
export const resolve = (...arg) => path.resolve(...arg)
```
:::
## package.json
`package.json` 中,我们需要进行以下声明:
- 声明 `type` 字段值为 `module`
这个字段声明了你的包将作为一个 `ECMAScript module``NodeJs` 加载并解析,并允许使用`.mjs`格式的文件。
- 声明 `exports` 字段
该字段描述了 项目如何导出模块给到其他包使用。
- 默认导出
::: code-tabs
@tab package.json
```json
{
"exports": "./lib/index.js"
}
```
:::
即当使用`import { resolve } from 'my-esm-package'`时,默认引入的文件是 `lib/index.js`
- 导出多个模块
::: code-tabs
@tab package.json
```json
{
"exports": {
".": "./lib/index.js",
"resolve": "./lib/resolve.js"
}
}
```
:::
声明了导出了两种模块:一个是默认导出,使用`"."` 作为key一个是具名导出。
当使用`import { resolve } from 'my-esm-package'`时,默认引入的文件是 `lib/index.js`
当使用`import { resolve } from 'my-esm-package/resolve'` 时,引入的文件是 `lib/resolve.js`
- `exports` 还支持其他形式的值,这里暂不赘述。
- 声明 `engines` 字段
由于 `Nodejs` 并不是全版本支持`esm`的,而是从`v14.16.0`版本开始试验性的支持,并到了`v16`版本才作为正式支持,
且当前`v16`版本作为目前的长期稳定支持的版本。这个项目运行环境的`NodeJs` 版本,最低应该推荐使用 `v16` 以上的版本。
即它的值应该为 `{ "node": ">=16" }`
到这里,这个项目的`package.json` 文件,包含以下内容:
::: code-tabs
@tab package.json
```json
{
"name": "my-esm-package",
"description": "My first esm package.",
"type": "module",
"exports": {
".": "./lib/index.js",
"resolve": "./lib/resolve.js"
},
"engines": {
"node": ">=16"
}
}
```
:::
## 编写项目代码
1. 由于是一个 `esm` 项目,所以理所当然的不能项目中使用 `require()`/`module.exports` 来导入导出模块。
而是应该全部使用`import`/`export` 的方式来导入导出模块。
2. 不需要在项目代码中 使用 `use strict`
3. 由于 `esm` 项目中,`NodeJs` 不再支持 `__dirname`/`__filename`,所以有相关场景需要使用时,需要使用其他的方式来实现相同功能:
```js
import { dirname, basename } from 'path'
import { fileURLToPath } from 'url'
const _dirname = typeof __dirname !== 'undefined'
? __dirname
: dirname(fileURLToPath(import.meta.url))
const _filename = typeof __filename !== 'undefined'
? __filename
: basename(fileURLToPath(import.meta.url))
```
## TypeScript
如果在项目中使用了 `TypeScript`,那么除了需要遵循以上的内容,还需要在 `tsconfig.json` 配置文件中补充以下配置:
```json
{
"module": "node16",
"moduleResolution": "node16"
}
```
并且,应该将 `.ts` 文件,编译为 `.js` 文件,`package.json` 配置的 `exports` 导出的,是编译后的 `.js` 文件。
## 最后
当完成了以上步骤,就可以得到一个`NodeJs ESM` 项目。它也只能在另一个支持 `esm` 的项目中使用。

View File

@ -1,108 +0,0 @@
---
title: JavaScript进阶— 函数参数按值传递
createTime: 2020/02/12 11:27:54
author: pengzhanbo
permalink: /article/m4a92nl5/
---
我们知道,在 `ECMAScrip` 中, 函数的参数是 **按值传递** 的。
那么怎么理解 **按值传递**
简单来说, **把函数外部的值复制给函数内部的参数**,即 **把值从一个变量复制到另一个变量**
那么也就是说,在函数内部,修改函数参数的值,不会改变外部变量的值。
我们来看一个例子2
**示例1**
```js
var a = 1
function foo(arg) {
arg = 2
console.log(arg)
}
foo(a) // 2
console.log(a) // 1
```
可以看出,外部变量`a`作为 函数 `foo` 的执行时参数值, 在函数内部修改传入的参数值进行修改,
函数执行后,并不会对外部变量`a` 发生修改。
这个例子确实说明了函数参数是按值传递的。
但是再来看另一个例子:
**示例2**
```js
var obj1 = {
a: 1
}
function foo(arg) {
arg.a = 2
console.log(arg)
}
foo(obj1) // { a: 2 }
console.log(obj1) // { a: 2 }
var obj2 = {
a: 1
}
function bar(arg) {
arg = 2
console.log(arg)
}
bar(obj2) // 2
console.log(obj2) // { a: 1 }
```
在这个例子中, 函数`foo` 执行完后, 打印的 `obj1` 值发生了变化,说明函数`foo` 内部修改了外部变量`obj1`
为什么会发生修改?而在 函数`bar` 执行后,`obj2` 值保持不变,这又是为什么? 函数参数是否真的是 **按值传递**
那么该如何理解 `函数参数是按值传递的`?
在理解这个之前,我们首先需要知道,`JavaScript` 的数据类型,以及不同数据类型的存储方式。
## 数据类型及其存储方式
我们知道, 在 `JavaScript` 中, 有两种 数据类型,分别是:**(1)基本数据类型**和 **(2)引用数据类型**
- 基本数据类型:值 直接保存在 **栈stack** 中。
```js
let a = 1
let b = a
a = 2
console.log(a, b) // 2 1
```
基本类型在 **栈** 中的赋值变动如下:
::: center
![function-value-stack](/images/func-value-stack.png){ style=width:500px; }
:::
- 引用数据类型:值 保存在 **堆heap** 中, 并在 **栈stack** 中保存 值 在 **堆heap** 中的内存地址。
```js
let a = { name: 'Mark' }
let b = a
b.name = 'John'
console.log(a) // { name: 'John' }
```
引用类型在 **栈****堆** 中的复制变动如下:
::: center
![function-value-stack](/images/func-value-heap.png){ style=width:680px; }
:::
## 按值传递
我们从 数据类型来理解 `按值传递`, 那么可以发现, **传递** 的值, 是指在 **栈stack** 中保存的值。
即, 无论 **参数值** 是 基本数据类型还是引用数据类型, **传递** 的是 **栈stack** 中的值。
- 对于基本数据类型, 函数内部修改参数的值,实际上是修改的是 函数参数重新在 **栈stack** 中的内存片段保存的值。
- 对于引用数据类型, 函数参数 传递是的 引用类型在 **栈stack** 中的内存地址:
- 如果直接修改参数的值,函数参数在 **栈stack** 中的内存片段保存的内存地址被覆盖。
- 如果修改 参数对象的属性值,修改的是根据 函数参数在 **栈stack** 中的内存片段保存的内存地址对应的在 **堆heap** 中的值。
所以回头重新看 **示例1****示例2** 均正确表述了 函数的参数是 **按值传递** 的。

View File

@ -1,159 +0,0 @@
---
title: JavaScript进阶— 原型到原型链
createTime: 2020/02/09 09:27:29
author: pengzhanbo
permalink: /article/hx3h2vvj/
---
`JavaScript` 的世界中,我们常常会通过 构造函数 来创建一个 **实例对象**
```js
function Person(name) {
this.name = name
}
const person = new Person('Mark')
console.log(person.name) // Mark
```
我们使用构造函数`Person`,通过 `new` 创建了一个实例对象 `person`
在实例对象 `person` 上, 有一个私有属性 `__proto__` 指向了它的构造函数的原型对象`prototype`
那么,什么是 `prototype`, 什么是 `__proto__` ?
接下来,我们开始进入正题。
## prototype
`JavaScript` 中,每个函数都有一个 `prototype` 属性,这个属性指向了该函数的原型对象。
```js
function Person(name) {
this.name = name
}
Person.prototype.age = 18
const person1 = new Person('Mark')
const person2 = new Person('John')
console.log(person1.name, person1.age) // Mark 18
console.log(person2.name, person2.age) // John 18
```
可以看到,当 `person1``person2` 均打印 `age` 属性值为 `18`
这是因为 `Person.prototype` 正是 实例对象 `person1``person2` 的原型。
既然 `prototype` 指向的是原型, 那么,**原型** 又是什么?
简单的理解,在 `JavaScript` 中,每一个对象在创建时,都有一个与之关联的另一个对象,这个关联的对象就是指原型。
::: warning 注意
`null` 是没有原型的。
:::
对象会从其原型对象,**继承** 属性。这也是为什么 `person1``person2` 均打印 `age` 属性值为 `18`
## \_\_proto\_\_
`JavaScript` 中,每个对象(`null` 除外)都有一个 私有属性 `__proto__`
这个属性指向了该对象的构造函数的原型`prototype`
```js
function Person(name) {
this.name = name
}
const person = new Person('Mark')
console.log(person.__proto__ === Person.prototype) // true
```
到这里就会发现,既然构造函数有原型 `prototype`,原型也是一个对象,而对象有 `__proto__` 指向它的构造函数的原型对象,
那么 构造函数的原型对象,是否也有其原型对象呢?
## constructor
在说明 原型的原型前,需要了解原型上的一个属性 `constructor`, 它指向了原型对象关联的构造函数:
``` js
function Person(name) {
this.name = name
}
const person = new Person('Mark')
console.log(person.prototype.constructor === Person) // true
```
它有助于帮我们理解 找到原型的原型的构造函数。
## 原型的原型
当我们在控制台打印并输出 `Person.prototype.__proto__` 时,发现打印了一个对象:
```js
function Person(name) {
this.name = name
}
console.log(Person.prototype.__proto__)
```
`output`:
![Person.prototype.__proto__](/images/js-prototype-1.png)
既然 `Person` 的原型也有原型, 那么 这个原型的原型对象,它的构造函数又是什么呢?
我们可以使用 `constructor` 来获取 它的构造函数:
```js
function Person(name) {
this.name = name
}
console.log(Person.prototype.__proto__.constructor)
```
`output`:
![Person.prototype.__proto__.constructor](/images/js-prototype-2.png)
可以发现 `Person` 的原型对象的原型对象,指向的构造函数是 `Object`
`Person.prototype` 的原型,指向的是 `Object.prototype`
那么, `Object.prototype` 有没有自己的原型呢?
![Object.prototype.__proto__](/images/js-prototype-3.png)
可以发现,`Object.prototype` 的原型,指向的是 `null`
`null` 是表示 **没有对象**,它没有原型。
## 原型链
`Person.prototype` -> `Object.prototype` -> `null`
(通过 `__proto__` 进行关联)
这种 对象有一个原型对象,它的原型对象又有它的原型对象,一层层如同链式一样,向上关联,称为 `原型链`
几乎所有 `JavaScript` 中的对象都是位于原型链顶端的 `Object` 的实例。
::: tip
`__proto__` 是一个非标准的属性,但绝大部分浏览器都支持通过这个属性来访问原型。
`__proto__` 在实现上是`Object` 上的一个 `getter/setter`访问器 ,使用 `obj.__proto__` 时,可以理解成返回了 `Object.getPrototypeOf(obj)`
:::
## 基于原型链的继承
`JavaScript` 对象是动态的属性“包”(指其自己的属性)。`JavaScript` 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
```js
function Person(name) {
this.name = name
}
Person.prototype.age = 18
const person1 = new Person('Mark')
const person2 = new Person('John')
console.log(person1.name, person1.age) // Mark 18
console.log(person2.name, person2.age) // John 18
```
在这个示例中, 虽然`Person` 的实例对象自身并没有`age` 属性,但由于它的原型对象上有 `age` 属性,
实例对象从它的原型对象 **继承** 了属性 `age` 。 从而其 `age` 属性的值为 `18`

View File

@ -1,227 +0,0 @@
---
title: JavaScript进阶— 执行上下文
createTime: 2020/02/12 04:35:52
author: pengzhanbo
permalink: /article/d12xkizf/
---
## 执行上下文
执行上下文是, `JavaScript` 代码被 **解析****执行****所在环境** 的抽象概念。
`JavaScript` 的任何代码都是在执行上下文中执行的。
### 类型
`JavaScript` 有三种 执行上下文 类型:
- **全局执行上下文**
默认的执行上下文,或者说基础执行上下文。 任何不在函数内部的代码,都是在 全局执行上下文中。
全局上下文执行两个事情:
- 创建一个全局的 `window`对象(在浏览器环境中)。
- 设置 `this` 的值等于 全局的 `window` 对象。
一个程序只会有一个全局执行上下文。
- **函数执行上下文**
每当函数被执行时,都会为该函数创建一个新的执行上下文。
每个函数都有它自己的执行上下文,且是在函数执行的时候进行创建。
函数上下文可以有任意多个,每当一个函数执行上下文被创建,它会按照定义的顺序,执行一系列步骤。
- **eval函数执行上下文**
执行在 `eval` 函数内部的代码也会有它属于自己的执行上下文。
### 创建执行上下文
创建执行上下文主要分为两个阶段: **创建阶段****执行阶段**
## 创建阶段
在创建阶段,会做三件事:
- this 值的决定,即 This绑定
- 创建词法环境组件
- 创建变量环境组件
### This绑定
- 全局执行上下文
在全局执行上下文中, `this` 的值指向全局对象。(在浏览器中, `this` 引用 `Window` 对象)。
- 函数执行上下文
在函数执行上下文中, `this` 的值取决于该函数是如何被调用的。如果它被一个引用对象调用,那么 `this` 会被设置成那个对象,否则 `this` 的值被设置为全局对象或者 `undefined` (在严格模式下)。
```js
let foo = {
bar: function() {
console.log(this);
}
}
// 'this' 引用 'foo', 因为 'baz' 被对象 'foo' 调用
foo.bar();
let bar = foo.baz;
// 'this' 指向全局 window 对象,因为没有指定引用对象
bar();
```
### 词法环境
> [ECMAScript 标准](https://262.ecma-international.org/6.0/)
>
> 词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符和具体变量和函数的关联。
> 一个词法环境由环境记录器和一个可能的引用外部词法环境的空值组成。
词法环境是一种 **持有标识符—变量映射** 的结构。
::: tip
这里的标识符指的是变量/函数的名字,而变量是对实际对象[包含函数类型对象]或原始数据的引用。
:::
在词法环境的内部有两个组件:**(1) 环境记录器**和 **(2) 一个外部环境的引用**。
- **环境记录器**是存储变量和函数声明的实际位置。
- **外部环境的引用**意味着它可以访问其父级词法环境(作用域)。
词法环境有两种类型:
- **全局环境**(在全局执行上下文中)是没有外部环境引用的词法环境。全局环境的外部环境引用是 `null`。它拥有内建的 Object/Array/等、在环境记录器内的原型函数(关联全局对象,比如 `window` 对象)还有任何用户定义的全局变量,并且 `this` 的值指向全局对象。
- 在**函数环境**中,函数内部用户定义的变量存储在环境记录器中。并且引用的外部环境可能是全局环境,或者任何包含此内部函数的外部函数。
环境记录器也有两种类型:
- **声明式环境记录器**: 存储变量、函数和参数。
- **对象环境记录器**: 用来定义出现在全局上下文中的变量和函数的关系。
可以看出:
- 在**全局环境**中,环境记录器是对象环境记录器。
- 在**函数环境**中,环境记录器是声明式环境记录器。
::: tip 注意
对于**函数环境****声明式环境记录器**还包含了一个传递给函数的 `arguments` 对象(此对象存储索引和参数的映射)和传递给函数的参数的 `length`
:::
使用伪代码描述 词法环境,大致如下:
```js
GlobalExecutionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 在这里绑定标识符
}
outer: <null>
}
}
FunctionExecutionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 在这里绑定标识符
}
outer: <Global or outer function environment reference>
}
}
```
### 变量环境
**变量环境** 同样是一个 **词法环境**,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。
**变量环境** 有着上面定义的词法环境的所有属性。
`ES6` 中,**词法环境**和**变量环境**的一个不同就是前者被用来存储函数声明和变量(`let``const`)绑定,
而后者只用来存储 `var` 变量绑定。
示例代码:
```js
let a = 20
const b = 30
var c
function multiply(e, f) {
var g = 20
return e * f * g
}
c = multiply(20, 30)
```
示例代码 执行上下文伪代码:
```js
GlobalExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 在这里绑定标识符
a: < uninitialized >,
b: < uninitialized >,
multiply: < func >
}
outer: <null>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 在这里绑定标识符
c: undefined,
}
outer: <null>
}
}
FunctionExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 在这里绑定标识符
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 在这里绑定标识符
g: undefined
},
outer: <GlobalLexicalEnvironment>
}
}
```
::: warning
只有遇到调用函数 `multiply` 时,函数执行上下文才会被创建。
:::
**说明:**
可能你已经注意到 `let``const` 定义的变量并没有关联任何值,但 `var` 定义的变量被设成了 `undefined。`
这是因为在创建阶段时,引擎检查代码找出变量和函数声明,虽然函数声明完全存储在环境中,但是变量最初设置为 `undefined``var` 情况下),或者未初始化(`let``const` 情况下)。
这就是为什么你可以在声明之前访问 `var` 定义的变量(虽然是 `undefined`),但是在声明之前访问 `let``const` 的变量会得到一个引用错误。
这就是我们说的变量声明提升。
## 执行阶段
在此阶段,完成对所有这些变量的分配,最后执行代码。
::: warning
在执行阶段,如果 `JavaScript` 引擎不能在源码中声明的实际位置找到 `let` 变量的值,它会被赋值为 `undefined`
:::

View File

@ -1,138 +0,0 @@
---
title: JavaScript进阶— 执行上下文栈
createTime: 2020/02/11 11:53:33
author: pengzhanbo
permalink: /article/6snk1b6c/
---
关于 执行上下文,请点击查看[这篇文章](/article/d12xkizf/)。
## 执行上下文栈
`JavaScript` 引擎 创建了 执行上下文栈 来存储并管理 代码执行时创建的所有 执行上下文。
执行上下文栈Execution context stackECS 是一个种拥有 LIFO(后进先出) 的栈。
我们使用一个数组 模拟 执行上下文栈:
```js
const ECSStack = []
```
`JavaScript` 执行时,首先遇到的是 全局代码,初始化时,会首先向 执行上下文栈中压入 全局执行上下文
global execution context)。它只有在整个程序结束时,才会被清空,所以在程序结束前, `ECSStack` 底部
都会有一个 `globalExecutionContext`
```js
const ECSStack = [
globalExecutionContext
]
```
`JavaScript` 开始执行以下代码时:
``` js
function foo() {
console.log('foo')
}
function bar() {
foo()
}
function run() {
bar()
}
run()
```
在这段代码中,`JavaScript` 进行以下处理:
``` ts
// 伪代码:
// run() 创建 函数执行上下文,并压入 执行上下文栈
ECSStack.push(functionExecutionContext<run>)
// run() 中发现需要执行 bar(), 继续创建 函数执行上下文,并压入执行上下文栈
ECSStack.push(functionExecutionContext<bar>)
// bar() 中发现需要执行 foo(), 继续创建 函数执行上下文,并压入执行上下文栈
ECSStack.push(functionExecutionContext<foo>)
/** 此时, ECSStack 的结构,如下:
* [
* globalExecutionContext,
* functionExecutionContext<run>,
* functionExecutionContext<bar>,
* functionExecutionContext<foo>
* ]
*/
// 当 foo() 执行完毕,从执行上下文栈中移除 functionExecutionContext<foo>
ECSStack.pop()
// 当 bar() 执行完毕,从执行上下文栈中移除 functionExecutionContext<bar>
ECSStack.pop()
// 当 run() 执行完毕,从执行上下文栈中移除 functionExecutionContext<run>
ECSStack.pop()
// 在程序未结束时, ECSStack 底部永远有一个 globalExecutionContext
```
再看下一个例子:
```js
var scope = "global scope";
function checkScope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkScope();
```
```js
var scope = "global scope";
function checkScope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkScope()();
```
这两段代码,虽然在执行结果是一样的,都是输出 `local scope` 但是在 执行上下文栈 中的变化不同。
第一段代码, 在 执行上下文栈,模拟处理如下:
```ts
// 执行checkScope()
ECSStack.push(functionExecutionContext<checkScope>)
// 在checkScope执行阶段中执行 f()
ECSStack.push(functionExecutionContext<f>)
ECSStack.pop()
ECSStack.pop()
```
第二段代码, 在执行上下文栈,模拟处理如下:
```ts
// 执行checkScope()
ECSStack.push(functionExecutionContext<checkScope>)
ECSStack.pop()
// 在checkScope 完成后,再执行 f()
ECSStack.push(functionExecutionContext<f>)
ECSStack.pop()
```
可以看出, 函数执行的时机不同,虽然最终结果一致,但是在 执行上下文栈 中的过程是不同的。
## 总结
1. 执行上下文栈,是用于存储和管理所有执行上下文。
2. 在程序没有结束前,执行栈中永远有一个全局执行上下文。
3. 函数执行时,会创建一个新的函数执行上下文,并按顺序压入到执行栈中。
4. 函数执行完成后,对应的函数执行上下文会从执行栈中移除。

View File

@ -1,38 +0,0 @@
---
title: JavaScript进阶— 词法作用域
createTime: 2020/02/10 11:37:25
author: pengzhanbo
permalink: /article/fpcpgpod/
---
## 作用域
作用域是指 程序源代码中,定义变量的区域。
作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。
## 词法作用域
`JavaScript` 中,采用的是 词法作用域, 即静态作用域。
词法作用域规定了,函数的作用域是在 **函数定义的时候就确定** 了。
### 示例
```js
var a = 1
function foo() {
console.log(a)
}
function bar() {
var a = 2
foo()
}
bar()
```
这个示例的执行结果为 `1`
在这个例子中, 由于 函数`foo` 的作用域在 定义的时候就确定了,即使在 函数`bar` 中也有相同的变量名`a`的定义,
但是由于两个函数在定义时,作用域是相互独立的,函数`foo`在其作用域查找局部变量`a`,没有找到,
继续从它书写位置往上查找上一层的代码,所以输出的结果为 `1`

View File

@ -1,94 +0,0 @@
---
title: webpack模块热替换HMR
createTime: 2021/03/24 05:14:18
author: pengzhanbo
permalink: /article/knagbtgd/
---
**模块热替换Hot Module Replacement** 是 webpack 的一个 十分有用且强大的 特性。
当我们对 文件代码进行修改并保存后webpack 会重新编译打包文件代码,并将新的模块代码发送给客户端,
浏览器通过将旧模块替换为新模块,实现在浏览器不刷新的情况下,对应用进行更新。
<!-- more -->
## 前言
在还没有 HMR 之前,我们对文件代码进行更新保存后,想要查看更新后的内容,常常需要手动刷新浏览器。
但还好的是,也有一些 **live reload** 的工具库,这些库能够监听文件的变化,通知浏览器刷新页面,
从而帮助我们减少了重复的操作。
但是为什么还需要 HMR 呢?
当浏览器刷新,也意味着当前页面的状态丢失了。
比如,我们打开了一个弹窗,然后我们对弹窗的代码逻辑进行了修改并保存,浏览器刷新后,弹窗被关闭了,
我们需要重新进行交互打开弹窗。
这无疑会增加非常多的重复且无意义工作量、时间。
HMR 的作用,就是不仅帮助我们在无刷新的情况下更新了应用代码,同时还保留了应用的状态,让我们能避免了
大量重复操作,从而提高开发效率。
## 模块热替换
模块热替换(HMR - Hot Module Replacement)功能会在应用程序运行过程中替换、添加或删除模块,而无需重新加载整个页面。
### 启用
启用 HMR 的方式很简单,[查看官方文档](https://www.webpackjs.com/guides/hot-module-replacement/)
### 特性
HMR有几个特性
- 保留在完全重新加载页面时丢失的应用程序状态。
- 只更新变更内容,以节省宝贵的开发时间。
- 调整样式更加快速 - 几乎相当于在浏览器调试器中更改样式。
## HMR基本流程
- **Step 1:**
webpack watch 模式下,监听文件系统中的某个文件是否发生修改。当监听到文件发生变更时,
根据配置文件**对模块进行重新编译打包**,并将打包后的代码 通过 JavaScript 对象保存在内存中。
- **Step 2:**
webpack-dev-middleware 调用 webpack 的API 对代码的变化进行监控并通知webpack将代码打包到内存中。
- **Step 3:**
webpack-dev-server 监听文件变化,不同于第一步的是,这一步不监听代码变化进行重新编译打包。
当配置文件中配置了 `devServer.watchContentBase``true` 时,
Server会监听配置的文件夹中静态文件的变化如果发生变化通知浏览器进行 `live reload`,即刷新页面。
- **Step 4:**
webpack-dev-server 通过 sockjs 在浏览器和服务器端之间建立一个 websocket 长连接,
将webpack编译打包的各个阶段的状态信息告知浏览器端也包括第三步中 Server 监听静态文件变化的信息。
浏览器端根据这些socket消息进行不同的操作。
其中,服务器传递的最主要的信息,是新模块的 hash 值,后续步骤根据 hash值 进行模块的替换。
- **Step 5:**
webpack-dev-server 虽然会告知浏览器打包状态,但在 webpack-dev-server/client 端并不会去请求更新的代码,
也不会执行热模块替换的操作,这些工作会交回给 webpack/hot/dev-server。
webpack/hot/dev-server 根据 webpack-dev-server/client 传给它的信息,以及 dev-server 的配置信息,
来决定是刷新浏览器,还是执行 热模块替换。
- **Step 6:**
在客户端中HotModuleReplacement.runtime 接受到 上一步传递给它的新模块的 hash 值,
通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求server 端返回一个 json。
该 json 包含了所有要更新的模块的 hash 值,获取到需要更新的模块列表后,再发送一个 jsonp 请求,
获取最新的模块代码。
- **Step 7:**
HotModulePlugin 会对新旧模块进行对比,决定是否更新模块。
在决定更新模块后,检查模块之间的依赖关系,更新模块的同时,也更新模块间的依赖引用。
这个步骤也决定了 HMR 是否成功。
- **Step 8:**
如果 HMR 失败,则回退到 live reload 操作,通过刷新浏览器来获取最新打包的代码。

View File

@ -1,176 +0,0 @@
---
title: webpack原理的简单入门
createTime: 2021/03/21 06:13:07
author: pengzhanbo
permalink: /article/gq88mn6a/
---
::: center
![webpack](https://www.webpackjs.com/32dc115fbfd1340f919f0234725c6fb4.png){width="100px"}
:::
## 前言
我们知道, `webpack` 作为前端工程化中,主流的模块打包工具之一,应用于各种各样的前端工程化项目中。
虽然大多数项目都或多或少会使用到 `webpack` 但是可能对于大多数的 前端开发人员来说,
可能只是改改 `webpack` 的配置, 或者甚至从未动过 `webpack` 的相关文件,
或多或少对 `webpack` 的配置以及功能感到陌生。
还有类似于 `vue-cli``create-react-app``umi.js` 等各种基于 `webpack` 封装的 脚手架,
提供了各种开箱即用的功能,这使得 `webpack` 离我们好像越来越远。
但是当我们的某个项目面临了不得不去 深入 `webpack` 才能解决的问题,或者 面试时,被问起 `webpack` 相关的问题,
就难以解决或者回答。
所以我们需要对 `webpack` 至少有基本的了解,了解它的原理、如何编写 `loader``plugin` 等。
## webpack是什么
> 引用 [webpack官网](https://webpack.js.org/concepts/)
>
> At its core, webpack is a static module bundler for modern JavaScript applications. When webpack processes your application, it internally builds a dependency graph from one or more entry points and then combines every module your project needs into one or more bundles, which are static assets to serve your content from.
>
> 本质上webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
从作用上讲webpack 的功能就是将不同模块的文件,打包整合到一起,并且保证它们之间引用的正确,且有序执行。
这使得我们在做项目架构时,能够从模块的角度去做文件拆分,然后交给 webpack 打包整合。
而一个项目中的文件,不仅有 html文件、CSS文件、JavaScript文件、图片资源、Vue特有的`.vue`文件typescript的`.ts` 文件等,以及项目的中的代码还需要进行压缩混淆、浏览器兼容、等等必要的处理,启动一个本地的开发服务器、模块的热更新替换等, 可以通过`webpack` 提供的各种机制,来一一实现。
对于 `webpack` 来说, 它自身只能识别 JavaScript 文件, 而对于其他的资源,可以通过 webpack提供的 `Loader` 特性来实现
识别。 通过 `Loader`,可以把其它类型的资源文件,转换为 webpack能够处理的有效模块。
而对于 代码混淆、本地开发服务器、模块热更新,则可以通过 webpack 提供的 `Plugin` 特性来实现功能上的扩展。
## 模块打包原理
在 webpack 中,有四个基础且核心的概念:
- **入口entry**
- **输出output**
- **加载器Loader**
- **插件Plugin**
### 入口entry
指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。
### 输出(output)
告诉 webpack 在哪里输出它所创建的 bundles以及如何命名这些文件
### 加载器Loader
webpack 自身只能理解 JavaScript 文件 和 json 文件, loader 可以将其他类型的资源文件转换为 webpack能够处理的有效模块。
本质上webpack loader 将所有类型的文件,转换为应用程序的依赖图(和最终的 bundle可以直接引用的模块。
### 插件Plugin
用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。
插件接口功能极其强大,可以用来处理各种各样的任务。
### 模块modules
在模块化编程中,开发者将程序分解成离散功能块(discrete chunks of functionality),并称之为模块。
对于 webpack ,任何文件都可以是一个模块。
### 模块打包运行原理
在说 webpack 的 **模块打包运行原理** 之前, 先看下 我们是如何使用 webpack的
一般情况下, 我们通过编写一个 配置文件`webpack.config.js` 对 webpack 进行本地化的配置,
大致的配置如下:
``` js
module.exports = {
// 声明模块的入口文件
entry: './src/entry.js',
output: {
path: path.resolve(__dirname, 'dist'), // 输出目录
filename: 'bundle.js', // 文件名称
},
module: {
rules: [
// 配置 使用 babel-loader 对 .js 资源进行转换
{
test: /\.js$/,
loader: 'babel-loader',
},
// ...more loader
],
},
// 插件配置
plugins: [
new EslintWebpackPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
// ...more plugin
],
// ...more config
}
```
`webpack` 读取了 配置文件后,运行的流程大致如下:
1. 读取 `webpack` 的配置参数;
2. 启动 `webpack` , 创建 `compiler` 对象,开始解析项目;
3. 从入口文件 `entry` 开始解析,并找到其导入的**依赖模块**,递归遍历分析,形成**依赖关系树**
4. 对不同的文件类型资源的依赖模块文件,使用对应的 `Loader` 进行转换,最终转为 webpack的有效模块
5. 在编译过程中, `webpack` 通过 发布订阅模式,向外抛出一些 `hooks` `webpack``Plugin` 通过监听各个 `hooks`
执行插件任务,扩展 `webpack` 的功能,干预输出结果。
6. 根据 输出配置 `output` ,将打包构建好的资源文件 输出。
`compiler` 对象是一个全局单例,负责控制整个 webpack 构建流程。
在构建过程中,还会产生一个当前构建的上下文对象 `compilation`, 它包含了当前构建的所有信息,在每个热更新或重新构建时, `compiler` 都会产生一个新的`compilation` 对象,负责当前构建过程。
每个模块间的依赖关系,则依赖于`AST`语法树。每个模块文件在通过`Loader`解析完成之后,
会通过`acorn`库生成模块代码的`AST`语法树,通过语法树就可以分析这个模块是否还有依赖的模块,
进而继续循环执行下一个模块的编译解析。
最终, webpack 打包构建出来的 bundle 文件,是一个 IIFE 执行函数。
```js
// webpack5下进行的最小化打包输出文件
(() => {
// webpack 模块文件内容
var __webpack_modules__ = ({
"entry.js": ((modules) => { /* ... */ }),
"other.js": ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => { /* ... */ })
});
// 模块缓存
var __webpack_module_cache__ = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = __webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {}
};
// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// Return the exports of the module
return module.exports;
}
// startup
// Load entry module and return exports
// This entry module can't be inlined because the eval devtool is used.
var __webpack_exports__ = __webpack_require__("entry.js");
})();
```
在上面的打包demo中整个立即执行函数里边只有三个变量和一个函数方法`__webpack_modules__`存放了编译后的各个文件模块的JS内容`__webpack_module_cache__` 用来做模块缓存,`__webpack_require__` 是Webpack内部实现的一套依赖引入函数。最后一句则是代码运行的起点从入口文件开始启动整个项目。
`__webpack_require__`模块引入函数,我们在模块化开发的时候,通常会使用`ES Module`或者`CommonJS`规范导出/引入依赖模块webpack打包编译的时候会统一替换成自己的`__webpack_require__`来实现模块的引入和导出,从而实现模块缓存机制,以及抹平不同模块规范之间的一些差异性。

View File

@ -1,332 +0,0 @@
---
title: 跨域资源共享(CORS)
createTime: 2020/08/29 07:40:31
author: pengzhanbo
tags:
- http
permalink: /article/2f45bq9x/
---
**跨域资源共享CORS** 是一种基于 **HTTP Header** 的机制。
该机制通过允许服务器标示除了它自己的 origin协议和端口使这些 origin 有权限访问加载服务器上的资源。
<!-- more -->
跨域资源共享 通过 **预检请求** 的机制,检查服务器是否允许要发送的真实请求。
浏览器向服务器发送一个到服务器托管的跨域资源 **预检请求**
在预检请求中浏览器发送的头部中标示有HTTP方法和真实请求会用到的头。
## 前言
浏览器出于安全性的原因,会限制脚本内发起的跨域资源请求,
比如 **XMLHttpRequest****Fetch API** 遵循 **同源策略**,默认情况下不允许发起非同源的资源请求。
使用这些API的Web应用只能加载从应用程序的同一个域的请求HTTP资源
**除非响应报文中包含了正确的CORS响应头**
## 概述
跨域资源共享 新增了一组 HTTP首部字段允许服务器声明哪些源站通过浏览器有权限访问哪些资源。
同时,对于可能对服务器数据产生副作用的 HTTP 请求方法,浏览器必须首先使用 `OPTIONS` 方法发起一个预检请求,
从而获取服务器是否允许跨域请求服务器确认允许之后才发起实际的HTTP请求。
CORS 请求失败会产生错误,但是为了安全,在 JavaScript 代码中,是无法获取具体是哪里出了问题。
我们只能通过查看浏览器的控制台来获取具体出现的错误。
若要开启 CORS ,我们需要配置 CORS 相关的 HTTP首部字段。
## HTTP 响应首部字段
在 CORS 中HTTP 响应首部字段主要有以下几个:
- **Access-Control-Allow-Origin**
- **Access-Control-Allow-Methods**
- **Access-Control-Allow-Headers**
- **Access-Control-Max-Age**
- **Access-Control-Expose-Headers**
- **Access-Control-Allow-Credentials**
### Access-Control-Allow-Origin
**Access-Control-Allow-Origin** 响应首部字段,用于 **指定允许访问该资源的外域URI**
对于不需要携带身份凭证的请求,服务器可以指定改字段的值为通配符(`*`),表示允许来自所有域的请求。
语法:
```
Access-Control-Allow-Origin: <origin>
Access-Control-Allow-Origin: *
```
如果服务器 指定了具体的域名而非 `*`,那么响应首部中的 **Vary** 字段的值必须包含 `Origin`
用于告诉客户端:服务器对不同的源站返回不同的内容。
::: info 注意
当响应的是附带身份凭证的请求时,服务端 必须 明确 **Access-Control-Allow-Origin** 的值,而不能使用通配符`“*”`
:::
**示例1**
允许所有域访问
```
Access-Control-Allow-Origin: *
```
**示例2**
允许来自 https://pengzhanbo.cn 的请求
```
Access-Control-Allow-Origin: https://pengzhanbo.cn
Vary: Origin
```
### Access-Control-Allow-Methods
**Access-Control-Allow-Methods** 响应首部字段用于 预检请求的响应。
**指明了实际请求所允许使用的HTTP方法或方法列表**。
语法:
```
Access-Control-Allow-Methods: <method>[, <method>]*
```
示例:
```
Access-Control-Allow-Methods: POST, GET, OPTIONS
```
### Access-Control-Allow-Headers
**Access-Control-Allow-Headers** 响应首部字段用于 预检请求的响应。
**指明了实际请求中允许携带的首部字段**。
语法:
```
Access-Control-Allow-Headers: <header-name>[, header-name]*
Access-Control-Allow-Headers: *
```
以下特定的首部是一直允许的,无需特意声明他们:
- Accept
- Accept-Language
- Content-Language
- Content-Type但只在其值属于MIME类型 `application/x-www-form-urlencoded`,`multipart/form-data`,`text/pain` 中的一种。
**示例1**
自定义请求头。 除了 CORS 安全清单列出的请求头外,支持 自定义请求头 X-Custom-Header
```
Access-Control-Allow-Headers: X-Custom-Header
```
**示例2**
多个自定义请求头。
```
Access-Control-Allow-Headers: X-Custom-Header, X-My-Header
```
### Access-Control-Max-Age
**Access-Control-Max-Age** 响应首部字段表示 **预检请求的返回结果可以被缓存多久**
返回结果是指: **Access-Control-Allow-Methods****Access-Control-Allow-Headers** 提供的信息。
语法:
```
Access-Control-Max-Age: <delta-seconds>
```
**delta-seconds** 表示返回结果可以被缓存的最长时间(秒)。
在 Firefox 中, 上限是 **24小时86400秒**
在 Chromium 中,上限是 **2小时7200秒**,同时 Chromium 还规定了默认值是 **5秒**
如果值为 **-1** 表示禁用缓存,则每次请求前都需要使用 OPTIONS 预检请求。
**示例**
将预检请求缓存 10分钟
```
Access-Control-Max-Age: 600
```
### Access-Control-Expose-Headers
**Access-Control-Expose-Headers** 响应首部字段,列出了 哪些首部可以作为响应的一部分暴露给外部。
在 跨源访问时XMLHttpRequest 对象的 `getResponseHeader()` 方法默认只能拿到一些最基本的响应头。
默认情况下,只有七种 简单响应首部 可以暴露给外部:
- Cache-Control
- Content-Language
- Content-Length
- Content-Type
- Expires
- Last-Modified
- Pragma
如果期望让客户端可以访问到其他的首部信息,可以将它们 该字段受列出来。
语法:
```
Access-Control-Expose-Headers: <header-name>[, <header-name>]*
```
**示例**
暴露一个非简单响应首部:
```
Access-Control-Expose-Headers: X-My-Header
```
暴露多个非简单响应首部:
```
Access-Control-Expose-Headers: X-My-Header, X-Custom-Header
```
### Access-Control-Allow-Credentials
**Access-Control-Allow-Credentials** 响应首部字段 用于在 请求包含 Credentials 时,
告知浏览器是否可以将对请求的响应暴露给前端 JavaScript 代码。
当请求的 Credentials 模式 Request.credentials`include` 时,浏览器尽在相应头 **Access-Control-Allow-Credentials** 的值为 `true` 时将响应暴露给前端的 JavaScript 代码。
Credentials 可以是 `cookies``authorization headers``TLS client certificates`
语法:
```
Access-Control-Allow-Credentials: true
```
**Access-Control-Allow-Credentials** 需要与 `XMLHttpRequest.withCredentials`
**Fetch API**`Request()` 构造函数中的 `credentials` 选项结合使用。
Credentials 必须在前后端都被配置,才能使带 credentials 的 CORS 请求成功。
**示例:**
允许 credentials
```
Access-Control-Allow-Credentials: true
```
使用带 credentials 的 XHR:
``` js
const xhr = new XMLHttpRequest()
xhr.open('GET', 'https://pengzhanbo.cn', true)
xhr.withCredentials = true
xhr.send(null)
```
使用带 credentials 的 Fetch:
``` js
fetch('https://pengzhanbo.cn', {
credentials: 'include'
})
```
## HTTP 请求首部字段
在 CORS 中,可用于发起跨域请求的首部字段,如下:
- Origin
- Access-Control-Request-Method
- Access-Control-Request-Headers
这些首部字段无需手动设置。
当开发者使用 XMLHttpRequest 发起跨域请求时,它们已经被设置就绪。
### Origin
**Origin** 请求首部字段表明预检请求或实际请求的源站。
语法:
```
Origin: <origin>
```
origin 参数的值为源站的URI。不包含任何路径信息仅表示服务器名称。
### Access-Control-Request-Method
**Access-Control-Request-Method** 请求首部字段用于预检请求。作用是 将实际情况所使用的HTTP方法告诉服务器。
语法:
```
Access-Control-Request-Method: <method>
```
### Access-Control-Request-Headers
**Access-Control-Request-Headers** 请求首部字段用于预检请求。作用是 将实际请求所携带的首部字段告诉服务器。
语法:
```
Access-Control-Request-Headers: <header-name>[, <header-name>]*
```
## 预检请求
一个 CORS 预检请求时用于 检查服务器使用支持 CORS 即 跨域资源共享。
预检请求 通过 发送一个 OPTIONS 请求,请求头部包含了以下字段:
- Access-Control-Request-Method
- Access-Control-Request-Headers
- Origin
浏览器会在有必要的时候,自动发出一个预检请求。
所以在正常情况下,前端开发者不需要自己去发送这样的请求。
### 预检请求与凭据
CORS 预检请求不能包含凭据。预检请求的响应必须指定 Access-Control-Allow-Credentials: true 来表明可以携带凭据进行实际的请求。
## 简单请求
某些情况下,不会触发 CORS预检请求这样的请求可表述为 _简单请求_
若请求满足以下所有条件,则可视为 简单请求:
- 使用 GET, HEAD POST 请求方法
- 除了被用户代理自动设置的首部字段ConnectionUser-Agent等
以及在 Fetch 规范中定义为 [禁用首部名称](https://fetch.spec.whatwg.org/#forbidden-header-name) 的其他首部,
允许人为设置的字段为 Fetch 规范定义的 对 [CORS 安全的首部字段集合](https://fetch.spec.whatwg.org/#cors-safelisted-request-header)
- 请求中任意的 XMLHttpRequest 对象均没有注册任何监听事件,
XMLHttpRequest 对象可以使用 XMLHttpRequest.upload 属性访问。
- 请求中没有使用 ReadableStream 对象。
## 附带身份的请求与通配符
在响应附带身份凭证的请求时:
- 服务器不能将 **Access-Control-Allow-Origin** 的值设为通配符 `*`而应将其设置为特定的域Access-Control-Allow-Origin: https://pengzhanbo.cn。
- 服务器不能将 **Access-Control-Allow-Headers** 的值设为通配符 `*`而应将其设置为首部名称的列表Access-Control-Allow-Headers: X-Custom-Header, Content-Type
- 服务器不能将 **Access-Control-Allow-Methods** 的值设为通配符 `*`而应将其设置为特定请求方法名称的列表Access-Control-Allow-Methods: POST, GET
## 需要CORS的场景
1. 使用 **XMLHttpRequest** 发起的 HTTP请求
2. 使用 **Fetch API** 发起的 HTTP 请求
3. Web字体CSS通过 `@font-face` 使用的跨域字体资源
4. WebGL 贴图
5. 使用 drawImage 将 Images/video 画面绘制到 canvas
6. 来自图像的 CSS 图形
## 安全
在实际的使用场景中,尽可能的少使用 通配符 `*`,来允许所有域访问,或允许所有自定义首部字段,
这可能在 web 安全上来带风险。

View File

@ -1,256 +0,0 @@
---
title: 内容安全策略(CSP)
createTime: 2020/08/28 03:25:32
author: pengzhanbo
tags:
- http
- 安全
permalink: /article/snkdmwsz/
---
内容安全策略(`Content-Security-Policy`),简称 `CSP`。是一种 计算机安全标准。
主要目标是 减少和报告XSS攻击、数据注入攻击等。这些攻击手段的主要目的是盗取网站数据、网站内容污染、散发恶意软件等。
几乎所有现在浏览器都支持 `CSP` 对于不支持的浏览器,则会忽略 `CSP`
<!-- more -->
## XSS攻击
XSS攻击是一种常见的、危害极大的网络攻击手段。它利用浏览器对从服务器获取的内容的信任
通过站点的 `script` 脚本、内联脚本、外部导入资源等方式进行注入攻击。
恶意脚本在受害者浏览器中执行,以达成其目的。
## CSP
`CSP` 通过 **有效域名**,即 **浏览器认可的可执行脚本的有效来源** ,使 服务器管理者有能力消除或减少 XSS 攻击所以来的载体。
支持 `CSP` 的浏览器,仅会执行从白名单域名加载的脚本文件,忽略其他所有脚本,包括内联脚本和 HTML 事件处理属性。
## 制定策略
`CSP` 通过 声明 HTTP 头部字段 `Content-Security-Policy` 来启用和配置策略:
```
Content-Security-Policy: policy;
Content-Security-Policy: policy; policy;
```
参数 `[policy]` 是一个包含了描述 各种CSP策略指令的字符串。
## 策略指令
### default-src
为其他CSP指令提供备选项如果其他指令不存在用户代理会查找并应用该值如果其他指令有配置值那么则不会应用 default-src的值。
default-src 策略允许指定一个或多个值:
```
Content-Security-Policy: default-src <source>;
Content-Security-Policy: default-src <source> <source>;
```
### script-src
脚本内容安全策略指令,包括限制 外部资源、内联脚本、eval函数。
```
Content-Security-Policy: script-src <source>
```
### style-src
CSS文件内容安全策略指令包括限制 内联样式表、通过`<link>` 引入的css文件、样式中通过 `@import` 导入的css文件、
元素的 `style` 属性、 `style.cssText` 属性、以及 `el.setAttribute('style', '')`
```
Content-Security-Policy: style-src <source>
```
### img-src
图片资源内容安全策略指令, 限制通过 `<img>` 加载的图片资源
```
Content-Security-Policy: img-src <source>
```
### media-src
媒体资源内容安全策略指令,限制通过 `<audio>``<video>``<track>` 加载的媒体资源
```
Content-Security-Policy: media-src <source>
```
### frame-src
iframe内容安全策略指令限制`<iframe>` 加载的页面资源
### 其他指令
- `manifest-src` 限制 manifest 资源(通过`<link>`引入的 manifest文件
- `worker-src` 限制 `worker`资源,包括 `Worker``SharedWorker``ServiceWorker`
- `child-src` 限制 `web worker``<frame>``<iframe>`
- `connect-src` 限制允许通过脚本接口加载的链接地址,包括:`<a>``Fetch``XMLHttpRequest``WebSocket``EventSource`
- `font-src` 限制 `@font-face` 加载字体的有效源规则。
- `object-src` 限制 `<object>``<embed>``<applet>`
### 指令`<source>`有效值
- `<host-source>`
以域名或者 IP 地址表示的主机名,外加可选的 URL 协议名URL scheme以及端口号。
支持前置通配符(星号 '*'),可以将通配符应用于站点地址、端口中,如应用于端口,则表示允许使用该域名下的所有端口。
- **example.com:443** 匹配 example.com 上 443 端口访问
- **https://example.com** 匹配使用了 http: 的 example.com 的访问
- ***.example.com** 匹配 example.com 下的所有子域名的访问
- `<scheme-source>`
协议名如'http:' 或者 'https:'。必须带有冒号,不要有单引号。
- `'self'`
指向与要保护的文件所在的源,包括相同的 URL scheme 与端口号。必须有单引号。
- `'unsafe-inline'`
允许使用内联资源,例如内联 `<script>` 元素javascript: URL、内联事件处理器以及内联 `<style>` 元素。必须有单引号。
- `'unsafe-eval'`
允许使用 eval() 以及相似的函数来从字符串创建代码。必须有单引号。
- `'none'`
不允许任何内容。 必须有单引号。
- `'nonce-<base64 值>'`
特定使用一次性加密内联脚本的白名单。服务器必须在每一次传输政策时生成唯一的一次性值。否则将存在绕过资源政策的可能。
- `<hash-source>`
使用 sha256、sha384 或 sha512 编码过的内联脚本或样式。其由用短划线分隔的两部分组成:用于创建哈希的加密算法,以及脚本或样式 base64 编码的哈希值。当生成哈希值的时候,不要包含 `<script>``<style>` 标签,同时注意字母大小写与空格——包括首尾空格——都是会影响生成的结果的。
```
Content-Security-Policy: default-src sha256-abcdef;
```
- `'strict-dynamic'`
strict-dynamic 指定对于含有标记脚本 (通过附加一个随机数或散列) 的信任,应该传播到由该脚本加载的所有脚本。与此同时,任何白名单以及源表达式例如 'self' 或者 'unsafe-inline' 都会被忽略。
## 启用CSP
启用CSP可以在 HTTP服务器中新增 Header 字段:
在nginx中
``` nginx
http {
# ...more
server {
# ...more
location / {
index index.html;
Content-Security-Policy default-src 'self';
# ...more
}
}
}
```
也可以在 html 文件中 添加 `<meta>` 标签
``` html
<meta http-equiv="Content-Security-Policy" content="default-src 'self';" />
```
## 示例
### 示例1
默认只允许加载本站资源
```
Content-Security-Policy: default-src 'self';
```
### 示例2
默认只允许加载本站资源,但允许任意来源图片资源
```
Content-Security-Policy: default-src 'self'; img-src *;
```
### 示例3
默认只允许加载本站资源,允许 script资源、css资源、图片资源从指定cdn域名加载
```
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' https://cdn.example.com; img-src 'self' https://cdn.example.com;
```
### 示例4
阻止所有 iframe 窗口,允许本站加载其他资源
```
Content-Security-Policy: default-src 'self'; frame-src 'none';
```
### 示例5
执行特定 nonce 的内联脚本:
```
Content-Security-Policy: script-src 'nonce-abcdef' 'self';
```
只有在`<script>`标签内带有特定 `nonce` 值的脚本才允许执行:
``` html
<script nonce="abcdef" src="example.js"></script>
```
### 示例6
Hash 值相符的脚本才能执行:
```
Content-Security-Policy: script-src 'sha256-qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng='
```
该hash值必须是 script 标签内容的 sha256 值,代码才能执行:
``` html
<script>
alert("Hello, world.");
</script>
```
## 违例报告
启用 CSP 后,默认情况下,违例报告不会发送。我们可以通过配置 `report-uri` 策略指令并提供至少一个URI地址去递交报告。
```
Content-Security-Policy: default-src 'self'; report-uri http://report.example.com/csp;
```
### 违例报告示例
违例报告将以 JSON 对象的数据结构进行递交:
``` json
{
"csp-report": {
"document-uri": "http://example.com/index.html", // 发生违规的文档的 URI
"referrer": "", // 违规发生处的文档引用(地址)
"blocked-uri": "http://example.com/css/style.css", // 被 CSP 阻止的资源 URI。
"violated-directive": "style-src cdn.example.com", // 违反的策略名称。
// 在 Content-Security-Policy HTTP 头部中指明的原始策略。
"original-policy": "default-src 'none'; style-src cdn.example.com; report-uri /_/csp-reports"
}
}
```
当服务器接收到 违例报告,可以通过分析报告内容,来进行自定义的处理。

View File

@ -1,90 +0,0 @@
---
title: 为你的站点开启HSTS
createTime: 2020/11/12 07:08:12
author: pengzhanbo
tags:
- http
- 安全
permalink: /article/1w4onzn1/
---
`HTTP-Strict-Transport-Security` 简称为 `HSTS`,是一个 HTTP 响应头。
用于通知浏览器应该只通过 HTTPS 访问该站点,并且以后使用 HTTP 访问该站点的所有尝试都应自动转换为 HTTPS。
<!-- more -->
## 中间人劫持
当用户在未知风险的网络环境中访问 某个网站的时候,如访问 `http://example.com`,在这个未知风险的网络环境中,
可能会被其他人拦截到用户发出的网络请求,然后跳转到一个一模一样的钓鱼网站,或者在请求内容中,注入有危害的代码、广告等,
这种攻击行为,被称为 **中间人劫持**
`example.com` 也支持 `https` 协议进行访问后,如果用户直接通过 `https` 协议访问,那么在一定程度上可以有效防止
`中间人劫持`
如果用户依然通过 `http` 协议访问,虽然服务器可以重定向到 `https` 请求,然而在这个过程中,中间人依然可以
通过拦截 `http` 请求,然后向服务器发起 `https` 请求获取内容,再注入新的内容 返回给用户。
用户在浏览器地址栏中 输入 `example.com`, 浏览器默认发起的是 `http` 请求,这导致了我们很难要求用户在通过域名访问
网站时,一定要输入 `https://example.com`
为了限制 `中间人劫持` 这种潜在的攻击手段,一种处理方式就是 强制浏览器使用 `https` 协议访问网站。
为此,我们需要给网站开启 `HSTS`
## HSTS
`HSTS` 通过声明 `HTTP` 头部字段 `HTTP-Strict-Transport-Security` 来启用和配置策略:
```
Strict-Transport-Security: max-age=<expire-time>
Strict-Transport-Security: max-age=<expire-time>; includeSubDomains
Strict-Transport-Security: max-age=<expire-time>; preload
```
### 指令
#### `max-age=<expire-time>`
设置在浏览器收到这个请求后的`<expire-time>`秒的时间内凡是访问这个域名下的请求都使用 HTTPS 请求。
#### `includeSubDomains` <Badge>可选</Badge>
如果这个可选的参数被指定,那么说明此规则也适用于该网站的所有子域名。
#### `preload` <Badge>可选</Badge>
查看 [预加载 HSTS](https://www.chromium.org/hsts/) 获得详情。不是标准的一部分。
### 浏览器处理
> 当网站已开启 `HSTS`
用户在第一次通过 `https` 协议访问网站时,服务器响应`Strict-Transport-Security` 头,浏览器记录下信息,
在以后重新访问访问网站时,会把访问这个网站的 `http` 请求自动替换为 `https`
`HSTS` 头设置的过期时间到了,后面通过 `HTTP` 的访问恢复到正常模式,不会再自动跳转到 `HTTPS。`
每次浏览器接收到 `Strict-Transport-Security` 头,它都会更新这个网站的过期时间,所以网站可以刷新这些信息,防止过期发生。
Chrome、Firefox 等浏览器里,当尝试访问该域名下的内容时,会产生一个 307 Internal Redirect内部跳转自动跳转到 HTTPS 请求。
## 预加载
如果用户首次访问网站时,依然使用的是 `http` 协议,浏览器会忽略`Strict-Transport-Security`,而且中间人依然可以劫持请求内容,删除 `Strict-Transport-Security`
为了进一步处理这个问题, `Google``Firefox` 等浏览器厂商,维护了一个 `HSTS` 预加载服务。
你可以将你已开启了 `HSTS` 的 站点域名,提交到 预加载服务中,浏览器将会永不使用非安全的方式连接到你的域名。
但是,这不是 HSTS 标准的一部分,也不该被当作正式的内容。
[`HSTS`预加载服务](https://hstspreload.org/)
## 示例
当前域名,以及所有子域名,开启 `HSTS`, 过期时间为 一年。
```
Strict-Transport-Security: max-age=31536000; includeSubDomains
```

View File

@ -1,331 +0,0 @@
---
title: HTTP缓存机制
createTime: 2019/08/24 12:18:39
author: pengzhanbo
permalink: /article/c3ez957l/
---
::: note
老生常谈!老生常谈!老生常谈啊!
:::
## 什么是HTTP缓存
当客户端向服务器发起资源请求时,会先抵达浏览器缓存,如果浏览器有要请求的资源的副本,
那么就可以直接从浏览器缓存中提取而不是从原始服务器中获取这个资源。
http缓存都是从对同一资源的第二次请求开始的。
- 第一次请求时,服务器返回资源,并在`response header`中回传资源的缓存参数;
- 第二次请求时,浏览器会根据这些缓存参数,判断是否使用浏览器缓存的资源副本还是从服务器获取资源。
## HTTP缓存分类
HTTP缓存根据是否需要重新向服务器发起请求可分为两大类
- 强缓存: 强制缓存,在缓存有效时间内,不再向服务器发起资源请求,直接使用浏览器缓存的资源副本
- 协商缓存:在缓存有效时间内,需要向服务器询问资源是否需要更新,如果需要更新,则从服务器获取新的资源,
如果不需要更新,则继续使用浏览器缓存的资源副本;
::: tip 另一种缓存分类
根据资源是否可以被单个用户或多个用户使用来分类,还可以分为 私有缓存和共享缓存。
这种一般是对于 代理服务器的,即 浏览器发起请求 -> 代理服务器 -> 原始服务器。
- 私有缓存: 在代理服务器中,仅针对单个用户使用的资源缓存,其他用户发起的对同一个资源的首次请求,仍然需要从原始服务器获取资源
并为该用户建立新的缓存资源。
- 共享缓存:只要有一个用户发起的对同一个资源的首次到达代理服务器的请求,代理服务器对该资源缓存后,其他用户请求代理服务器上的资源,
在缓存有效时间内,代理服务器不再向原始服务器获取新的资源,返回代理服务为缓存的资源副本。
:::
## 主要的HTTP Headers
- 通用首部字段
| 字段 | 说明 |
| -- | -- |
| Cache-Control | 控制缓存行为 |
| Pragma | http1.0时代的产物,值为 no-cache 时禁用缓存 |
- 请求头部字段 Request Headers
| 字段 | 说明 |
| -- | -- |
| If-Match | 比较 ETag 是否一致 |
| If-None-Match | 比较 ETag 是否不一致 |
| If-Modified-Since | 比较资源最后更新时间是否一致 |
| If-Unmodified-Since | 比较资源最后更新时间是否不一致 |
- 响应头部字段 Response Headers
| 字段 | 说明 |
| -- | -- |
| ETag | 资源匹配信息 |
- 实体头部字段
| 字段 | 说明 |
| -- | -- |
| Expires | http1.0时代的产物,实体主体过期时间 |
| Last-Modified | 资源的最后一次更新时间 |
::: warning 提醒
`Pragma``Expires` 这两个header是 http1.0中的内容,在 http1.1及往后的版本中逐步被弃用。
但为了能够对浏览器向下兼容,大多数网站在设置 缓存机制时,仍然在 response headers 中保留这两个字段的声明。
本文同样也会对这两个字段进行说明以及为什么http1.1后会使用 `Cache-Control` 代替。
:::
::: warning 提醒
在某些技术文章分享中常常会直接把这些headers字段各自分类到 强缓存 或 协商缓存中,
个人认为这种简单粗暴的划分方式是有待商榷,就比如`Cache-Control`的不同取值,其行为会根据值表现为强缓存或协商缓存。
:::
### Pragma
`Pragma` 字段仅有一个 `no-cache`的可选值,会告知客户端不要对该资源进行缓存读取,应该每次都向服务器发送资源请求。
在客户端使用时,通常做法是在 HTML中加上一个 meta 标签:
``` html
<meta http-equiv="Pragma" content="no-cache">
```
::: caution 警告
- 这个标签声明仅有 IE才能识别含义其他主流浏览器不兼容。
- 在IE浏览器中虽然能够识别含义但并不一定会在请求Request Header中加上Pragma但确实会让当前页面每次都发起新请求。
仅限页面html文件页面内使用的其他资源不受影响。
:::
在服务端配置为 Response Header 时,浏览器读取到该字段,会禁用缓存行为,后续的对同一资源的请求会重新发起请求而不使用缓存。
::: warning 提醒
由于`Pragma` 在浏览器端的兼容问题在服务器端又有其他字段能更好的控制缓存行为Pragma 字段基本已经被抛弃,不再使用,
*除了部分网站出于兼容性考虑,还会带上该字段。*
:::
### Expires
在 http1.0中Pragma 用于禁用缓存也需要有一个字段用于启用缓存和定义缓存时间。Expires 就是用于这个目的。
Expires 的值是一个 GMT时间 如:`Thu Jun 07 2018 14:26:45 GMT`,用于告诉浏览器资源的缓存过期时间,如果还没有超过该时间
则不发起新的资源请求。
在客户端,可以使用 meta标签来告知浏览器缓存时间
``` html
<meta http-equiv="expires" content="Thu Jun 07 2018 14:26:45 GMT">
```
如果希望不走缓存,每次页面请求都发起新的请求,可以把 content 设置为 -1 或 0。
::: caution 提醒
跟 Pragma 字段一样, 该 meta 标签只有 IE 能够正确识别。
而且该方式仅是告知 IE 缓存时间的标记,并不能在 Request Header 中找到该字段。
:::
服务端在 Response Headers 中设置 Expires 字段,则在任何浏览器中都能正确设置资源缓存时间;
::: info 说明
如果同时使用 Pragma 和 Expires 字段, 则 Pragma 优先级会更好,页面会发起新的请求
:::
::: warning 提醒
Expires 字段虽然能够定义缓存有效时间,但是这个时间的设置是相对于本地时间的。
如果在服务端定义,则这个时间是相对于服务端时间的,
这个时间返回到客户端, 客户端是拿着客户端的本地时间与返回的服务端时间做对比。
那么就会导致一种情况,当用户更改了客户端的时间,如超过了 Expires定义的缓存时间那么缓存就立即失效了。
也正是应该存在着这样的问题Expires并不能保证缓存能够达到预期的表现所以也被逐步弃用。
:::
### Cache-Control
`Cache-Control` 是从 `http1.1` 开始支持的 header 属性,该属性的值描述了使用缓存的行为以及缓存的有效时间。
`Cache-Control` 可以在 发起请求时,在`Request Headers` 中声明该属性,(如果资源请求是通过代理服务器再到原始服务器,)
通知代理服务器对资源的缓存方式,以及是否向原始服务器请求最新的资源。
`Cache-Control` 做为 `Response Headers` 属性返回时,通知浏览器对该资源的缓存方式和有效时间。
Cache-Control 语法如下:
```
Cache-Control: <cache-directive>
```
- 作为 `Request Headers` 时, `cache-directive` 支持以下可选值
| 字段名称 | 说明 |
| -- | -- |
| no-cache | 告知(代理)服务器不直接使用缓存,要求从原始服务器发起请求 |
| no-store | 所有内容都不会被保存到缓存或 Internet临时文件中 |
| max-age=delta-seconds | 告知服务器 客户端希望接收一个存在时间age不大于 delta-seconds 秒的资源|
| max-stale\[=delta-seconds] | 告知(代理)服务器 客户端愿意接收一个超过缓存时间的资源若有定义delta-seconds则为delta-seconds秒若没有则为超过任意时间 |
| min-fresh=delta-seconds | 告知(代理)服务器 客户端希望接收一个在delta-seconds秒内被更新过的资源 |
| no-transform | 告知(代理)服务器 客户端希望获取一个实体数据没有被转换(如压缩)过的资源 |
| only-if-cached | 告知(代理)服务器 客户端希望获取缓存的资源(若有),而不用向原服务器发起请求 |
- 作为 `Response Headers`时,`cache-directive` 支持以下可选值
| 字段名称 | 说明 |
| -- | -- |
| public | 表明任何情况下都需要缓存该资源 |
| private[="file-name"] | 表明返回报文中全部或部分(若指定了*file-name*的字段数据)仅开放给某些用户(服务器指定的*share-use*)做缓存使用,其他用户则不能缓存这些数据 |
| no-cache | 不直接使用缓存,要求向服务器发起(新鲜度校验)请求 |
| no-store | 所有内容都不会被保存到缓存或 Internet临时文件中 |
| max-age=delta-seconds | 告知客户端该资源在*delta-seconds*秒内是新鲜的,无需向服务器发起请求 |
| s-max-age=delta-seconds| 同 max-age但仅应用于 共享缓存 |
| no-transform | 告知客户端缓存文件时不得对实体数据做任何改变 |
| must-revalidate | 当前资源一定是向原始服务器发去验证请求的若请求失败会返回504(而非代理服务器上的缓存) |
| proxy-revalidate | 和 must-revalidate类似但仅应用于 共享缓存 |
- 可以直接在 HTML页面的`<head>` 中通过 meta标签来给请求头加上 `Cache-Control` 字段:
``` html
<meta http-equiv="Cache-Control" content="no-cache">
```
- `Cache-Control` 允许自由组合可选值:
```
Cache-Control: max-age=3600, must-revalidate
```
这段声明表示,该资源必须从原始服务器获取,且其缓存有效时间为一个小时,在后续的一个小时内,用户重新访问该资源都无需发送请求。
### 缓存校验
`Pragma``Expires``Cache-Control` 字段能够让客户端决定是否向服务器发送请求,缓存未过期的从本地缓存获取资源,缓存过期的从服务器端获取资源。
但是,客户端向服务器发送了请求,是否以为着一定要读取并返回该资源的实体内容?
- 如果一个资源在客户端的缓存时间过期了,但服务器并没有更新过这个资源,那服务端是否一定要重新把资源的实体内容返回?
- 如果这个资源过大,虽然缓存过期,但又没有更新过,返回实体内容是否会浪费带宽和时间?
对于这些问题,其实只要采取某种策略,让服务器知道客户端现在保存的缓存文件跟服务端的资源文件是一致的,
然后通知客户端该资源可以继续使用缓存文件,不需要重新返回资源实体内容。
那么就可以解决上述的问题同时为HTTP请求带来优化和加速。
http1.1 新增了 `Last-Modified``ETag``If-Match``If-None-Match``If-Modified-Since`
`If-Unmodified-Since` 这些字段,用于对缓存资源的校验,提高缓存的复用率。
### Last-Modified
服务器将资源发送给客户端时,会将资源的最后更新时间以如下格式加载实体首部,一起返回给客户端。
客户端会为该资源标记上该信息,下次在请求时,会把该信息添加在请求报文中发送给服务端去做检查。
如果客户端上报字段时间值和服务端的对应资源的最后修改时间一致,则说明改资源没有被修改过,直接返回 304状态码。
客户端在上报 Last-Modified 时,可以使用的 Request Headers 字段有两个:
- `If-Modified-Since`: 该字段格式如下
```
If-Modified-Since: <Last-Modified-Value>
```
字段告诉服务端如果客户端上报的最后修改时间和服务器上的最后修改时间一致则直接返回304和响应报头即可。
当前各浏览器默认使用该字段用来向服务端上报保存的 Last-Modified 值。
- `If-Unmodified-Since`: 该字段格式如下
```
If-Unmodified-Since: <Last-Modified-Value>
```
字段告诉服务端,如果客户端上报的最后修改时间和服务端上的最后修改时间不一致,
则应当返回 412Precondition Failed状态码给客户端。
Last-Modified 由于是使用的资源最后修改时间来确定资源是否有被修改,
但是在实际情况中,往往存在着一个资源被修改了但实际内容没有发生改变,
而由于资源最后修改时间已经发生改变,依然会返回整个实体内容给客户端,而其实内容跟客户端缓存内容一致。
### ETag
为了解决 `Last-Modified` 可能存在的不准确的问题http1.1 还推出了 ETag 实体首部字段。
服务器会通过某种算法,给资源计算得出一个唯一标识符,在把资源响应给客户端的时候,会在实体首部加上该字段一起返回给客户端。
```
ETag: ETag-Value
```
客户端为资源标记上该信息,下次在请求时,会把该信息添加在请求报文中发送给服务端去做检查。
服务端只需要比较客户端传来的ETag和对应的该资源的ETag是否一致就可以判断资源相对于客户端资源是否被修改过。
如果ETag是一致的那么就直接返回304状态码否则就返回新的资源实体内容给客户端。
客户端在上报 ETag 时,可以使用的 Request Headers 字段有两个:
- `If-None-Match` 该字段格式如下
```
If-None-Match: <ETag-Value>
```
字段告诉服务端如果ETag没有匹配上需要重新返回新的资源实体内容否则直接返回 304 状态码。
当前各浏览器默认使用该字段用来向服务端上报保存的 ETag 值。
- `If-Match` 该字段格式如下
```
If-Match: <ETag-Value>
```
字段告诉服务端如果ETag没匹配到或者收到了`"*"`值而当前没有该资源实体,
则应当返回412Precondition Failed状态码给客户端。否则服务器直接忽略该字段。
::: tip 提醒
如果 `Last-Modified``ETag` 同时被使用,则要求它们的验证必须同时通过才返回 304
若其中一个没有通过则服务器会按照常规返回资源的实体以及200状态码。
:::
## 次要的 HTTP Headers
以下的字段虽然跟缓存有关系,但没有那么重要。
### Vary
`Vary` 表示 服务端会以什么基准字段来区分、筛选缓存版本。
首先考虑一个问题服务端有一个请求地址如果是IE用户则返回针对IE开发的内容否则返回另一个主流浏览器版本的内容。
一般来说,服务端获取到请求的 `User-Agent` 字段做处理即可。
但是如果用户请求的是代理服务器而非原始服务器且代理服务器如果直接把缓存的IE版本资源发给了非IE的客户端那就出问题了。
而 Vary 则是用于处理这类问题的头部字段,只需要在响应报文加上:
```
Vary: User-Agent
```
字段告知代理服务器需要以 User-Agent 这个请求头部字段来区别缓存版本,确定传递给客户端的版本。
Vary 字段也接受条件组合的形式
```
Vary: User-Agent, Accept-Encoding
```
字段告知代理服务器需要以 User-Agent 和 Accept-Encoding 两个请求头部字段来区别缓存版本。
### Date、Age
Date 字段表示原始服务器发送该资源的响应报文时间GMT时间
该字段的作用可以帮助我们判断该资源命中的是原始服务器还是代理服务器。
- 如果`Date`的时间与当前时间差别较大或者连续F5刷新发现Date值没有变化那么说明当前请求命中的是代理服务器的缓存。
- 如果每次刷新页面浏览器每次都会重新发起这条请求那么其Date的值会不断变化说明该资源是直接从原始服务器返回的。
Age 字段表示某个文件在代理服务器中存在的时间如果文件被修改或替换Age会重新从0开始累计。
## 浏览器表现
### 强缓存
对于强缓存的资源:
- 当用户第一次访问该资源时,服务器返回 200状态码以及资源实体内容。
- 如果用户访问完第一次后,在没有关闭浏览器的前提下,进行了第二次或更多次资源访问,那么浏览器不再请求服务器,
而是从 浏览器的内存缓存区取出资源,并且 状态码 标记为 `200 (memory cache)`
- 如果用户访问完第一次后,关闭浏览器后,重新打开浏览器,进行第二次或更多次资源访问,那么浏览器也不会请求服务器,
- 而是从 浏览器的磁盘缓存区取出资源,并且 状态码 标记为 `200disk cache`
### 协商缓存
- 当用户第一次访问该资源时,服务器返回 200状态码以及资源实体内容。
- 如果用户进行第二次访问时,进行缓存校验。 或在缓存时间内,或 资源未被修改,那么 直接返回 304状态码
- 如果用户进行第二次访问时,服务器资源已被更新,则返回 状态码 200 ,以及新的资源实体内容。

View File

@ -1,165 +0,0 @@
---
title: 一些好玩的库或者框架汇总
createTime: 2022/05/27 09:16:16
author: pengzhanbo
permalink: /article/ci39ae1o/
---
这里收集了一些各种类型的好玩的库或者框架。
<!-- more -->
----
## Client Framework
### Lit
[![](https://lit.dev/images/logo.svg){style="width:40px"}](https://lit.dev/)
**lit** 是一个简单的、高效的、用于构建 web component 的轻量级库。
它替代了 [polymer](https://github.com/Polymer/polymer) 成为 WebComponent/customElement开发的首选库。
::: code-tabs
@tab my-element.ts
```ts
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
@customElement("my-element")
export class MyTimer extends LitElement {
static styles = css`...`;
@property() count = 0;
render() {
return html`<div>${this.count}</div>`;
}
}
```
@tab index.html
```html
<!doctype html>
<head>...</head>
<body>
<my-timer count="7"></my-timer>
</body>
```
:::
### solid-js
[solid-js](https://www.solidjs.com/)
一个用于构建用户界面简单高效、性能卓越的JavaScript库。
Solid 站在 React, Knockout 等巨人的肩膀上。如果你之前用 React Hooks 开发过Solid 应该看起来很自然。事实上Solid 模型更简单,没有 Hook 规则。每个组件执行一次,随着依赖项的更新,钩子和绑定会多次执行。
Solid 遵循与 React 相同的理念,具有单向数据流、读/写隔离和不可变接口。但是放弃了使用虚拟 DOM使用了完全不同的实现。
> 号称比 react 还 react 的库
```ts
import { render } from "solid-js/web";
import { onCleanup, createSignal } from "solid-js";
const CountingComponent = () => {
const [count, setCount] = createSignal(0);
const interval = setInterval(
() => setCount(count => count + 1),
1000
);
onCleanup(() => clearInterval(interval));
return <div>Count value is {count()}</div>;
};
render(() => <CountingComponent />, document.getElementById("app"));
```
### inferno
[inferno](https://www.infernojs.org/) 是一个快速的、类似于 React 的库,用于在客户端和服务器上构建高性能用户界面。
```tsx
import { render, Component } from 'inferno';
class MyComponent extends Component {
constructor(props) {
super(props);
this.state = {
counter: 0
};
}
render() {
return (
<div>
<h1>Header!</h1>
<span>Counter is at: { this.state.counter }</span>
</div>
);
}
}
render(
<MyComponent />,
document.getElementById("app")
);
```
### cycle-js
[cycle.js](https://cycle.js.org/)
Cycle.js是一个极简的JavaScript框架,提供了一种函数式,响应式的人机交互接口。
Cycle.js 有别于其他如 React/Vue 等框架,它提供的是一套完整的开发范式,需要在其范式基础上进行开发,相对来说并不
容易入门,但其函数式、响应式的思想,会带来非常好的启示和学习。
```js
import {run} from '@cycle/run'
import {div, label, input, hr, h1, makeDOMDriver} from '@cycle/dom'
function main(sources) {
const input$ = sources.DOM.select('.field').events('input')
const name$ = input$.map(ev => ev.target.value).startWith('')
const vdom$ = name$.map(name =>
div([
label('Name:'),
input('.field', {attrs: {type: 'text'}}),
hr(),
h1('Hello ' + name),
])
)
return { DOM: vdom$ }
}
run(main, { DOM: makeDOMDriver('#app-container') })
```
### svelte
[svelte](https://svelte.dev/)
Svelte 是一种全新的构建用户界面的方法。传统框架如 React 和 Vue 在浏览器中需要做大量的工作,而 Svelte 将这些工作放到构建应用程序的编译阶段来处理。
与使用虚拟virtualDOM 差异对比不同。Svelte 编写的代码在应用程序的状态更改时就能像做外科手术一样更新 DOM。
```svelte
<script>
let count = 0;
function handleClick() {
count += 1;
}
</script>
<button on:click={handleClick}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
```

View File

@ -0,0 +1,15 @@
---
title: 加密文章密码123456
author: pengzhanbo
createTime: 2023/12/25 01:28:29
permalink: /article/enx7c9s/
tags:
- 预览
- 标签
---
这是一篇 示例文章。
<!-- more -->
无内容。

View File

@ -0,0 +1,15 @@
---
title: 示例文章
author: pengzhanbo
createTime: 2023/12/25 01:28:29
permalink: /article/xld23tu1/
tags:
- 预览
- 标签
---
这是一篇 示例文章。
<!-- more -->
无内容。

View File

@ -1,55 +0,0 @@
---
title: 面试2
createTime: 2022/04/03 17:48:00
author: pengzhanbo
permalink: /article/exavsmm1
---
::: code-tabs
@tab yarn
``` bash
yarn add
```
@tab npm
``` bash
npm install
```
:::
::: tip
提示
:::
::: info
信息
:::
::: note
注释
:::
::: warning
警告
:::
::: caution
危险
:::
::: details
详情
:::
- [ ] todo
- [x] todo
::: normal-demo 普通代码
```html
<h3>这是一个代码片段</h3>
<span>???</span>
```
:::

View File

@ -1,60 +0,0 @@
---
title: 面试题以及个人答案 JS篇
tags:
- 面试
createTime: 2022/03/26 11:46:50
permalink: /article/4ml7z17g
author: pengzhanbo
top: false
type: null
---
### JS变量声明方式有哪些有什么区别
声明变量的方式有三种,分别是`var``let``const`。其中`let``const``es6`新增的变量声明方式。
`var`声明的变量的作用域是它当前执行的上下文中,并且存在变量提升
``` js
function fn() {
console.log(a);
var a = 1;
console.log(a);
}
fn();
```
相当于
``` js
function fn() {
var a;
console.log(a); // undefined
a = 1;
console.log(a); // 1
}
fn();
```
并且在可以重复声明同一变量在该声明的上下文中不会丢失其值。通过var声明的全局变量会作为窗口对象的属性。
`let` 声明的变量是块级作用域,不存在变量提升,并存在暂时死区,不允许在同一块级作用域重复声明。
``` js
function fn() {
console.log(a); // ReferenceError: a is not defined
let a = 1;
let a = 2; // TypeError thrown
console.loga(a);
}
fn();
```
`const` 声明一个常量,其作用域可以是全局或者是本地声明的块级作用域,与`var`变量不同,全局常量不会变为窗口对象的属性。常量必须在声明的同时初始化。同时声明创建的值是一个只读引用,但不意味着它所持有的值是不可变的,只是变量标识符不能重新分配。如果引用内容是对象的情况下,则可以改变对象内容,除非使用`Object.freeze()`方法冻结对象。`const`声明常量同样存在暂时死区。
____
### JS数据类型隐式转换
————
### JS模块化各自的优劣与不同点
____
### EVENT LOOP

View File

@ -1,152 +0,0 @@
---
title: 面试题以及个人答案 CSS篇
tags:
- 面试
createTime: 2022/03/26 11:46:50
permalink: /article/565o1wn0
author: pengzhanbo
top: false
type: null
---
### CSS什么是盒模型盒模型有哪些具体的表现和不同点是什么
盒模型是CSS规范定义的模块它规定了一个矩形盒子标准盒模型描述任意元素在文档树中占据的空间区域。每个盒子有四个边`外边距边margin edge or outer edge``边框边border edge``内填充边padding edge``内容边content edge or inner edge`,可以划分四个区域`外边距区域margin area``边框区域border area``内填充区域padding area``内容区域content area`
![css box model](https://drafts.csswg.org/css-box-3/images/box.png)
为什么会有盒模型类型严格来说多数浏览器都按照规范实现了标准盒模型而盒模型的类型主要是来自于不同浏览器对元素宽高的方式不同而导致IE浏览器认为元素的`width/height`应该是由元素的`内容+内填充+边框`组成而W3C规定的元素的`width/height`应该是元素的`内容`,从而衍生了不同的盒子模型。到`CSS3`,添加了`box-sizing`属性用于更改用于计算元素宽高的默认盒子模型并将IE浏览器和W3C规范纳入了实现中。可以使用此属性来模拟不正确支持CSS盒子模型规范的浏览器的行为。
_注`width/height`最终并不能完全决定元素的实际占用宽高。_
``` css
/* 关键字值 */
box-sizing: border-box; /* 默认值 */
box-sizing: content-box;
/* 全局值 */
box-sizing: inherit;
box-sizing: initial;
box-sizing: unset;
```
`border-box`规定了元素的`width``内容+内填充+边框`组成即IE浏览器的实现。 元素的实际占据宽度由 width属性+外边距。内容宽度为`width - padding - border`
`content-box`规定了元素的`width``内容宽度`, W3C规范的标准。元素的实际占据宽度由`widht + padding + border + margin`。内容宽度为`width`
`box-sizing`还有一个待废除的值`padding-box``width``height` 属性包括内容和内边距但是不包括边框和外边距。只有Firefox实现了这个值它在Firefox 50中被删除。
在高度计算上以上规则同样适用,但对非替换行内元素,尽管内容周围存在内边距与边框,但其占用空间受到`line-height`属性影响。
____
### CSS: 什么是外边距合并?什么情况下会发生外边距合并?
块元素的上外边距和下外边距有时候会发生合并,其大小取其中绝对值最大的值,这种行为叫做外边距合并。
__浮动元素__ 和 __绝对定位元素__ 的外边距不会发生合并。这是因为触发了 __块格式化上下文__
1. 相邻元素之间的外边距会发生合并(如果后一个元素需要清除前面的浮动,则不一定发生合并)。
2. 父元素与其第一个子元素之间不存在边框、内边距、行内内容、没有创建 __块格式化上下文__、没有清除浮动或者父元素与其最后一个子元素之间不存在边框、内边距、行内内容、heigh、min-height、max-height那么子元素的外边距会溢出到父元素外面。
3. 如果一个块级元素不包含任何内容并且在不存在边框、内边距、行内内容、heigh、min-height则该元素的上下外边距会发生合并。
三种情况的外边距合并是可以组合产生更加复杂的外边距合并情况的。
_如果外边距合并的值都是负值则合并的值为最小的外边距的值。_
_如果发生外边距合并的值包含负值则合并后的值为最大的正外边距与最小的负外边距之和。_
_____
### CSS垂直水平居中
_这是个老生常谈的问题了场景可以有很多答案也有很多答案而言其实本身不重要重要是明白为什么这个方法为什么可以实现垂直居中。_
__设立一个场景在一个宽高不固定的容器中实现一个宽高不固定的内容盒子并垂直水平居中。__
``` html
<!-- 假设 warpper、container 宽高不固定 实现container相对于wrapper垂直水平居中-->
<div class="wrapper">
<div class="container">
</div>
</div>
```
__方法一__ 使用 flex 布局
``` css
.wrapper{
display: flex;
}
.container{
margin: auto;
}
```
适用于支持 flex布局的浏览器IE11以上其他现代浏览器。这里是利用flex弹性布局的特性弹性容器改变了其子元素填充可用空间的方式子元素默认从容器左上角开始排列在不设置宽高时子元素填充空间由`flex`声明,默认值为`0 1 auto`,即
`flex-grow: 0;flex-shrink: 1;flex-basis: auto`; 其中 `flex-basis`定义了子元素的宽和高的尺寸大小,`auto`值表示自动尺寸,根据子元素内容计算宽高,在子元素上设置`margin: auto`,这是利用`auto`平均分配水平或垂直方向上的额外的空间,从而达到目的。(此方法实现的结果是“真正的”垂直水平居中)
或者
``` css
.wrapper{
display: flex;
justify-content: center;
align-content: center;
}
```
__方法二__ 使用 table 布局
``` css
.wrapper{
display: table-cell;
vertical-align: middle;
}
.container{
margin: auto;
}
```
利用的是table布局的特性不过该方法有个缺点就是`display: table-cell`元素的宽高设置百分比数值是“无效的”,原因是父元素非`table`元素或`display: table`元素,`display: table-cell`元素的宽高百分比数字是相对于`table`计算的。
__方法三__ `position` + `transform`
``` css
.wrapper{
position: relative;
}
.container{
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
```
该方法与前面两个方法的作用机理有很大的不同,首先第一点是`container`脱离了文档流,并且`container`自身的宽高发生了坍塌,在不设置宽高属性下,尺寸由内容撑开,`container`相对`wrapper`元素进行绝对定位,水平方向与垂直方向上,`container`的左上角顶点偏移到`wrapper`中点,`container``transform`是相对于自身的,` translate(-50%, -50%)`相对于自身,将左上角顶点做左上偏移自身的一半,从而实现了目的。
_有一些面试者给出了`container`元素上设置`margin-left: -50%; margin-top: -50%`的答案然而margin的百分比值是相对于其父元素计算的。_
__方法四__ 使用 行内块元素
``` css
.wrapper{
text-align: center;
}
.wrapper:after{
content: '';
display: inline-block;
vertical-align: middle;
height: 100%;
}
.container{
display: inline-block;
vertical-align: middle;
text-align: left;
}
```
该方法实现的垂直水平居中其实是一个近似垂直水平居中兼容IE7以上的浏览器。水平方向上`.wrapper`设置`text-align: center;`实现了水平居中;垂直方向上,给定`container`声明行内块元素,并`vertical-align: middle`,但由于`container`高度不确定无法声明具体的行高所以借助了父元素的伪类元素创建了一个宽度为0高度为100%的行内块元素,从而使`container`元素在垂直方向上实现了居中。但由于`vertical-align: middle`是元素的中线与字符X的中心点对齐大多数字体设计字体的中心点偏下也导致了实现的垂直居中并不是绝对的垂直居中。而要实现绝对的垂直居中需要添加一下属性
```css
.wrapper{
font-size: 0;
white-space: nowrap;
}
.container{
font-size: 14px; /* 重置回默认字体大小 */
white-space: normal;
}
```
实现方法有很多,这里暂时只列出其中的四种。
____
_想到还有其他问题继续补充..._

View File

@ -1,6 +1,6 @@
---
title: 主题效果预览
author: Plume Theme
author: pengzhanbo
createTime: 2023/12/25 01:28:29
permalink: /article/0lk24ty5/
sticky: true
@ -86,7 +86,7 @@ H~2~O
[外部链接](https://github.com/pengzhanbo)
![](/images/1px-lines.png)
![](/plume.png)
**Badge**

View File

@ -1,5 +1,6 @@
---
home: true
externalLink: false
config:
-
type: hero
@ -17,60 +18,149 @@ config:
-
theme: alt
text: Github
link: /
link: https://github.com/pengzhanbo/vuepress-theme-plume
-
type: features
title: 标题
description: 随便描述
features:
-
title: 特性1
icon: 🖨
details: 特性说明
title: 响应式布局
icon: 💻
details: 适配移动设备PC平板
-
title: 特性1
icon: 🖨
details: 特性说明
title: 博客 & 文档
icon: 📖
details: 无论是想写博客,或想写产品文档,或者两者兼顾
-
title: 特性1
icon: 🖨
details: 特性说明
title: 开箱即用
icon: 🚀
details: 支持零配置即可使用,也支持丰富的自定义配置
-
title: 特性1
icon: 🖨
details: 特性说明
title: 多语言
icon:
details: 内置了 中文/英文支持,还可以自定义添加更多的语言支持
-
title: 特性1
icon: 🖨
details: 特性说明
-
type: custom
-
type: text-image
title: 标题
description: 随便描述
list:
title: 双色主题
icon: 👨‍💻
details: 支持 浅色/深色 主题,包括代码高亮
-
title: 描述
description: 随便描述一下
- 随便描述一下
- 随便描述一下
image: /images/blogger.png
title: 插件
icon: 📦
details: 内置丰富的插件,一站式解决网站一般需求
-
title: 搜索、评论
icon: 🔍
details: 支持多种评论系统支持本地搜索、Algolia搜索
-
title: 加密
icon: 🔒
details: 支持全站加密、部分加密(加密目录、加密文章)
-
title: Markdown 增强
icon: 📝
details: 支持 Markdown 语法,支持 代码块分组、提示容器、任务列表、数学公式、代码演示等
-
type: image-text
title: 标题
description: 随便描述
title: 功能
description: 内置丰富的功能,满足网站一般需求。
image: /images/plume-1.svg
list:
-
title: 描述
description: 随便描述一下
- 随便描述一下
- 随便描述一下
image: /images/blogger.png
title: 文章信息
description: 为文章添加标签、分类、字数统计、阅读时间、写作日期等信息。
-
title: 评论
description: 支持 4 种评论系统,你可以自由选择符合你的需求的评论系统。
-
title: 搜索
description: 支持基于 minisearch 的本地搜索, 支持Algolia搜索。
-
title: 加密
description: 支持全站加密、部分加密(加密目录、加密文章)。
-
title: 代码复制
description: 一键复制代码块中的内容
-
type: profile
name: 鹏展博
description: 前端开发工程师, 热爱前端, 热爱生活, 热爱互联网, 热爱技术, 热爱开源, 热爱生命。
type: text-image
title: 博客
description: 主题默认支持博客,生成你的个人博客。
image: /images/plume-2.svg
list:
-
title: 文章列表
description: 通过文章写作日期,自动排序并生成博客文章列表页。
-
title: 博主信息
description: 自定义名称、座右铭、头像,社交媒体链接。
-
title: 标签、归档
description: 自动生成标签页,为文章根据年份进行归档。
-
type: image-text
title: 文档
image: /images/plume-3.svg
description: 主题默认支持文档,生成你的产品文档,或归纳你的知识体系。
list:
-
title: 侧边栏
description: 根据文档目录自动生成侧边栏,也可以手动配置,主题提供了更简单的配置方式。
-
title: 文档分类归纳
description: 可以文档的不同,归纳到不同的目录,更好的管理文档结构。
-
type: custom
---
这里是自定义的内容,你可以随意添加你自己的内容
<div style="max-width: 960px;margin:0 auto;">
::: center
![GitHub Repo stars](https://img.shields.io/github/stars/pengzhanbo/vuepress-theme-plume)
![npm version](https://img.shields.io/npm/v/vuepress-theme-plume?color=32A9C3&labelColor=1B3C4A&label=npm)
![npm downloads](https://img.shields.io/npm/dy/vuepress-theme-plume?color=32A9C3&labelColor=1B3C4A&label=downloads)
![github license](https://img.shields.io/github/license/pengzhanbo/vuepress-theme-plume?color=32A9C3&labelColor=1B3C4A)
:::
### 安装
:::code-tabs
@tab pnpm
```sh
pnpm add vuepress@next vuepress-theme-plume vue
```
@tab npm
```sh
npm install vuepress@next vuepress-theme-plume
```
@tab yarn
```sh
yarn add vuepress@next vuepress-theme-plume
```
:::
### 配置
::: code-tabs
@tab .vuepress/config.ts
```ts
import { defineUserConfig } from 'vuepress'
import { plumeTheme } from 'vuepress-theme-plume'
export default defineUserConfig({
// vuepress config...
theme: plumeTheme({
// theme config...
})
})
```
:::
### 更新记录
[Changelog](https://github.com/pengzhanbo/vuepress-theme-plume/blob/main/CHANGELOG.md)
### 贡献者
[![Contributor pengzhanbo](https://img.shields.io/badge/Contributor-pengzhanbo-32A9C3?style=for-the-badge&labelColor=1B3C4A)](https://github.com/pengzhanbo/)
[![Contributor huan_kong](https://img.shields.io/badge/Contributor-huan__kong-32A9C3?style=for-the-badge&labelColor=1B3C4A)](https://github.com/huankong233)
</div>

View File

@ -1,17 +1,26 @@
---
home: true
banner: /images/bg-home.jpg
hero:
name: pengzhanbo
tagline: Front-End Developer
text: 简单介绍专业技能信息相关的描述
actions:
-
theme: brand
text: Blog
link: /en/blog/
-
theme: alt
text: Github
link: /
externalLink: false
config:
-
type: hero
full: true
background: filter-blur
hero:
name: Theme Plume
tagline: Vuepress Next Theme
text: A simple, feature-rich vuepress document & blog theme
actions:
-
theme: brand
text: Quick Start →
link: /
-
theme: alt
text: Github
link: https://github.com/pengzhanbo/vuepress-theme-plume
-
type: custom
---
Todo...

View File

@ -0,0 +1,8 @@
---
title: Configuration
author: pengzhanbo
createTime: 2024/03/02 10:48:14
permalink: /en/config/intro/
---
Todo...

View File

@ -0,0 +1,9 @@
---
title: Theme Introduction
author: pengzhanbo
createTime: 2024/03/04 11:06:24
permalink: /en/guide/intro/
---
Todo...

View File

@ -1,8 +0,0 @@
---
title: plugin-caniuse
author: Plume Theme
createTime: 2023/06/15 09:33:00
permalink: /en/note/vuepress-plugin/caniuse
---
## Can I Use

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

View File

@ -1,6 +0,0 @@
---
title: README
createTime: 2022/04/04 11:13:30
author: pengzhanbo
permalink: /note/
---

Some files were not shown because too many files have changed in this diff Show More