Merge branch 'theme-next'

This commit is contained in:
pengzhanbo 2023-02-13 09:30:32 +08:00
commit 016d092ee0
350 changed files with 14581 additions and 8178 deletions

View File

@ -21,6 +21,7 @@ module.exports = {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'vue/component-tags-order': [
'error',
{

2
.gitignore vendored
View File

@ -13,3 +13,5 @@ dist/
*.log
*.tsbuildinfo
.mind
packages/theme-back

View File

@ -24,6 +24,7 @@
"composables",
"Docsearch",
"esbuild",
"frontmatter",
"gsap",
"iarna",
"leancloud",

View File

@ -1,5 +1,5 @@
import * as path from 'path'
import { themePlume } from '@vuepress-plume/vuepress-theme-plume'
import themePlume from '@vuepress-plume/vuepress-theme-plume'
import { viteBundler } from '@vuepress/bundler-vite'
import { webpackBundler } from '@vuepress/bundler-webpack'
import { defineUserConfig } from '@vuepress/cli'
@ -19,31 +19,33 @@ export default defineUserConfig({
theme: themePlume({
logo: 'https://pengzhanbo.cn/g.gif',
hostname: 'https://pengzhanbo.cn',
appearance: true,
avatar: {
url: '/images/blogger.jpg',
name: 'Plume Theme',
description: 'The Theme for Vuepress 2.0',
},
social: {
email: 'volodymyr@foxmail.com',
github: 'pengzhanbo',
QQ: '942450674',
weiBo: 'https://weibo.com',
zhiHu: 'https://zhihu.com',
facebook: 'https://baidu.com',
twitter: 'https://baidu.com',
linkedin: 'https://baidu.com',
},
social: [{ icon: 'github', link: 'https://github.com/pengzhanbo' }],
// {
// email: 'volodymyr@foxmail.com',
// github: 'pengzhanbo',
// QQ: '942450674',
// weiBo: 'https://weibo.com',
// zhiHu: 'https://zhihu.com',
// facebook: 'https://baidu.com',
// twitter: 'https://baidu.com',
// linkedin: 'https://baidu.com',
// },
notes,
darkMode: true,
navbar: [
{ text: 'Blog', link: '/blog/', activeMatch: '/blog/' },
{
text: 'VuePress',
children: [
items: [
{ text: 'theme-plume', link: '/note/vuepress-theme-plume/' },
{
text: 'Plugin',
children: [
items: [
{ text: 'caniuse', link: '/note/vuepress-plugin/caniuse/' },
{
text: 'netlify-functions',
@ -56,7 +58,7 @@ export default defineUserConfig({
],
footer: {
copyright: 'Copyright © 2022-present pengzhanbo',
content: '',
message: '',
},
themePlugins: {
search: {

View File

@ -1,6 +1,8 @@
import { definePlumeNotesConfig } from '@vuepress-plume/vuepress-theme-plume'
export default definePlumeNotesConfig({
dir: 'notes',
link: '/note',
notes: [
{
text: '',
@ -10,22 +12,22 @@ export default definePlumeNotesConfig({
'',
{
text: '指南',
children: ['快速开始', '编写文章'],
items: ['快速开始', '编写文章'],
},
{
text: '配置',
children: [
items: [
{
text: '主题配置',
link: '主题配置',
children: ['主题插件配置', 'notes配置'],
items: ['主题插件配置', 'notes配置'],
},
'页面配置',
],
},
{
text: '功能',
children: ['基础功能', 'markdown增强'],
items: ['基础功能', 'markdown增强'],
},
],
},
@ -39,7 +41,7 @@ export default definePlumeNotesConfig({
dir: 'netlify-functions',
text: 'plugin-netlify-functions',
link: 'netlify-functions',
children: ['', '介绍', '使用', '功能', 'API', 'functions开发指南'],
items: ['', '介绍', '使用', '功能', 'API', 'functions开发指南'],
},
],
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

@ -1,16 +1,14 @@
---
title: BFC 块级格式化上下文
createTime: 2018/05/17 12:28:33
permalink: /article/o5g7ggvf
permalink: /article/o5g7ggvf/
author: pengzhanbo
top: false
tags:
- html
type: null
---
## 概念
BFC, Block Formating Context。是 W3C CSS2.1规范中的一个概念。 是页面中的一块块级渲染区域,并且有一套渲染规则,它决定了其子元素将如何定位,以及和其他元素的关系和作用。
BFC, Block Formatting Context。是 W3C CSS2.1规范中的一个概念。 是页面中的一块块级渲染区域,并且有一套渲染规则,它决定了其子元素将如何定位,以及和其他元素的关系和作用。
具有BFC特性的元素可以看做是一个被隔离了的独立容器容器内的元素不会在布局上影响到外面的元素并且BFC具有普通容器所没有的一些特性。
@ -35,4 +33,4 @@ BFC, Block Formating Context。是 W3C CSS2.1规范中的一个概念。 是页
1. 同一个BFC的外边距会发生折叠合并 通过将其放在不同的BFC中规避折叠。
2. BFC可以包含浮动元素即清除浮动。
3. BFC可以阻止元素被浮动元素覆盖。
3. BFC可以阻止元素被浮动元素覆盖。

View File

@ -1,12 +1,10 @@
---
title: CSS At-Rules
createTime: 2018/10/06 08:16:38
permalink: /article/btkqop1a
permalink: /article/btkqop1a/
author: pengzhanbo
tags:
- css
top: false
type: null
---
## @charset
@ -160,4 +158,4 @@ type: null
## @media
媒体查询,详见 [CSS @media 媒体查询](/post/fe5ruia1/)
媒体查询,详见 [CSS @media 媒体查询](/post/fe5ruia1/)

View File

@ -1,12 +1,10 @@
---
title: CSS 媒体查询
createTime: 2018/08/18 08:43:02
permalink: /article/fe5ruia1
permalink: /article/fe5ruia1/
author: pengzhanbo
tags:
- css
top: false
type: null
---
开发响应式网站时,常常需要使用到 media 媒体查询。这里总结下媒体查询的使用方法。

View File

@ -1,12 +1,10 @@
---
title: CSS选择器
createTime: 2018/09/20 03:29:20
permalink: /article/8vev8ixl
permalink: /article/8vev8ixl/
author: pengzhanbo
tags:
- css
top: false
type: null
---
## Basic Selectors 基础选择器
@ -644,4 +642,4 @@ type: null
}
</style>
...
```
```

View File

@ -1,12 +1,10 @@
---
title: <!DOCTYPE> 文档类型声明
createTime: 2018/03/14 01:06:52
permalink: /article/s8udp6vp
permalink: /article/s8udp6vp/
author: pengzhanbo
tags:
- html
top: false
type: null
---
Web世界中随着历史的发展技术的迭代发展出了许多不同的文档只有了解文档的类型浏览器才能正确的解析渲染文档。
@ -105,4 +103,4 @@ http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
``` html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
```
```

View File

@ -1,12 +1,10 @@
---
title: HTML5新特性
createTime: 2018/02/17 12:49:58
permalink: /article/8rv45yuy
permalink: /article/8rv45yuy/
author: pengzhanbo
tags:
- html
top: false
type: null
---
## 语义标签
@ -272,4 +270,4 @@ history.replaceState({}, 'bar', 'bar.html')
})
```
[History API - Web API 接口参考 | MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/History_API)
[History API - Web API 接口参考 | MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/History_API)

View File

@ -2,13 +2,11 @@
title: WebComponent——template
lang: zh-CN
tags:
- html
- WebComponent
- javascript
createTime: 2018/8/2 11:15:27
permalink: /article/5fmy4kla
permalink: /article/5fmy4kla/
author: pengzhanbo
top: false
type: null
---
在web开发领域中模板并不少见。从服务器端的模板语言`Django``jsp`等,应用十分广泛,存在了很长时间。又如前端,早期例如`art(artTemplate)`以及近年来大多数的MV*框架涌现,绝大多数在展现层使用了同样的渲染机制:模板。

View File

@ -1,13 +1,11 @@
---
title: WebComponent——custom elements
tags:
- html
- WebComponent
- javascript
createTime: 2018/08/01 11:15:27
permalink: /article/m63fd7lf
permalink: /article/m63fd7lf/
author: pengzhanbo
top: false
type: null
---

View File

@ -0,0 +1,239 @@
---
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,12 +1,10 @@
---
title: meta 标签说明
createTime: 2018/03/15 01:21:48
permalink: /article/bp1nxjs6
permalink: /article/bp1nxjs6/
author: pengzhanbo
tags:
- html
top: false
type: null
---
<meta> 标签提供关于 HTML 文档的元数据。它不会显示在页面上,但是对于机器是可读的。可用于浏览器(如何显示内容或重新加载页面),搜索引擎(关键词),或其他 web 服务。
@ -216,4 +214,4 @@ content="app-id=APP_ID,affiliate-data=AFFILIATE_ID,app-argument=SOME_TEXT">
<meta name="theme-color" content="#E64545">
<!-- 添加到主屏 -->
<meta name="mobile-web-app-capable" content="yes">
```
```

View File

@ -1,17 +1,15 @@
---
title: 继承与原型链
createTime: 2018/07/06 09:40:54
permalink: /article/extends-prototype
permalink: /article/extends-prototype/
author: pengzhanbo
tags:
- javascript
top: false
type: null
---
当谈到继承时javascript只有一种结构对象。
每个实例对象object都有一个私有属性\_\_proto\_\_指向它的构造函数的原型对象(__prototype__)
该原型对象也有自己的原型对象\_\_proto\_\_层层向上直到有一个的原型对象为null。根据定义null没有原型并作为这个原型链的最后一个环节。
每个实例对象object都有一个私有属性`__proto__`指向它的构造函数的原型对象 __prototype__
该原型对象也有自己的原型对象`__proto__`层层向上直到有一个的原型对象为null。根据定义null没有原型并作为这个原型链的最后一个环节。
<!-- more -->
@ -20,3 +18,165 @@ type: null
## 基于原型链的继承
### 继承属性
`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

@ -2,7 +2,7 @@
title: 正则表达式
lang: zh-CN
createTime: 2018/11/26 11:15:27
permalink: /article/e8qbp0dh
permalink: /article/e8qbp0dh/
author: pengzhanbo
tags:
- javascript
@ -56,7 +56,7 @@ _以下讨论以正则表达式字面量来创建正则表达式_
元字符也叫特殊字符,是正则表达式规定的,对符合特定的单一的规则的字符的描述。
| 字符 | 含义 |
|:----:|-----|
|\\|在非特殊字符的前面加反斜杠,表示这个字符是特殊的,不能从字面上解释。比如在`\d`描述的不是一个普通的字符`d`,而是正则表达式中的数值`0-9`<br/>如果在特殊字符前面加反斜杠,这表示将这个字符转义为普通字符,比如`?`在正则中有其特殊含义,前面加反斜杠\?,这可以将其转为普通的`?`。|
| \\ |在非特殊字符的前面加反斜杠,表示这个字符是特殊的,不能从字面上解释。比如在`\d`描述的不是一个普通的字符`d`,而是正则表达式中的数值`0-9`<br/>如果在特殊字符前面加反斜杠,这表示将这个字符转义为普通字符,比如`?`在正则中有其特殊含义,前面加反斜杠\?,这可以将其转为普通的`?`。|
|^|匹配文本开始的位置,如果开启了多行标志,也会匹配换行符后紧跟的位置。<br/>比如`^a`会匹配`abc`,但不会匹配到`bac`。|
|$|匹配文本结束的位置,如果开启了多行标志,也会匹配换行符前紧跟的位置。<br/>比如`b$`会匹配`acb`,但不会匹配到`abc`。|
|*|匹配前一个表达式0次到多次。<br/>比如,`ab*`会匹配到`abbbbbbc`中的`abbbbbb`,以及`acbbbbb`中的`a`。|

View File

@ -1,8 +1,189 @@
---
title: Event Loop 浏览器端的事件循环
createTime: 2021/06/03 01:53:17
permalink: /article/browser-event-loop
permalink: /article/browser-event-loop/
author: pengzhanbo
top: false
type: null
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,10 +1,10 @@
---
title: 详解 Promise
createTime: 2020/11/22 12:58:28
permalink: /article/q40nq4hv
permalink: /article/q40nq4hv/
author: pengzhanbo
sticky: true
type: null
tags:
- javascript
---
## 概述
@ -47,6 +47,8 @@ Promise 创建后,必然处于以下几种状态
*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` 时触发。
@ -90,6 +92,162 @@ promise
})
```
## 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` 静态方法

View File

@ -1,13 +1,12 @@
---
title: 1px解决方案
createTime: 2019/05/15 10:41:32
permalink: /article/tz7ncicn
permalink: /article/tz7ncicn/
author: pengzhanbo
tags:
- html
- css
- develop
banner: /images/big-banner.jpg
---
在日常移动端前端应用开发中,经常遇到一个问题就是 1px的线在移动端 Retina屏下的渲染并未达到预期。以下总几种不同场景下的 1px解决方案。

View File

@ -1,13 +1,13 @@
---
title: lerna使用
createTime: 2021/11/26 06:28:37
permalink: /article/i1wc1uld
permalink: /article/i1wc1uld/
author: pengzhanbo
top: false
type: null
---
![lerna](https://user-images.githubusercontent.com/645641/79596653-38f81200-80e1-11ea-98cd-1c6a3bb5de51.png)
::: center
![lerna](https://user-images.githubusercontent.com/645641/79596653-38f81200-80e1-11ea-98cd-1c6a3bb5de51.png){ style="width: 50%" }
:::
## 概述
@ -118,4 +118,4 @@ lerna run build # 相当于在 package1、package2 中执行 npm run build
```
### lerna clean
删除所有包的node_modules
删除所有包的node_modules

View File

@ -0,0 +1,280 @@
---
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

@ -0,0 +1,135 @@
---
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

@ -0,0 +1,285 @@
---
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

@ -0,0 +1,60 @@
---
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,12 +1,10 @@
---
title: 移动端适配方案
createTime: 2020/08/14 01:54:29
permalink: /article/vhpmovsm
permalink: /article/vhpmovsm/
author: pengzhanbo
tags:
- develop
top: false
type: null
---
## 背景
@ -108,4 +106,4 @@ css像素是一个抽象单位主要用在浏览器上用来精确的度
1. 容器适配
2. 文本适配
3. 大于1px的边框、圆角、阴影
4. 内边距和外边距
4. 内边距和外边距

View File

@ -0,0 +1,143 @@
---
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

@ -2,12 +2,10 @@
title: Jenkins 使用
lang: zh-CN
createTime: 2018/09/16 11:15:27
permalink: /article/bmtl5ah4
permalink: /article/bmtl5ah4/
author: pengzhanbo
tags:
- 工具
top: false
type: null
---
[Jenkins](https://jenkins.io/) 是一款功能强大的应用程序,允许持续集成和持续交付项目。这里记录一些 Jenkins 使用的方法。

View File

@ -0,0 +1,35 @@
---
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,13 +1,12 @@
---
title: caniuse
createTime: 2021/02/07 06:41:12
permalink: /article/h4z91gyz
createTime: 2020/11/01 06:41:12
permalink: /article/h4z91gyz/
author: pengzhanbo
top: false
type: null
article: false
---
### 工具
将caniuse 的feature 结果以图片或者iframe的形式嵌入到站点。
[https://caniuse.bitsofco.de/](https://caniuse.bitsofco.de/)
[https://caniuse.bitsofco.de/](https://caniuse.bitsofco.de/)

View File

@ -0,0 +1,136 @@
---
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

@ -2,12 +2,10 @@
title: VSCode 常用插件推荐
lang: zh-CN
createTime: 2018/12/29 11:15:27
permalink: /article/ofp08jd8
permalink: /article/ofp08jd8/
author: pengzhanbo
tags:
- VSCode
top: false
type: null
---
`VS Code` 作为我现在工作中最常用的编辑器,也是我十分喜欢的编辑器。它强大的功能和插件系统,对我的工作提供了很多帮助和支持。将我在工作中经常使用的插件,推荐给大家。
@ -126,3 +124,5 @@ type: null
2. [minapp](https://github.com/wx-minapp/minapp-vscode)
微信小程序 支持。

View File

@ -0,0 +1,36 @@
---
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

@ -0,0 +1,108 @@
---
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

@ -4,10 +4,8 @@ lang: zh-CN
tags:
- vue
createTime: 2018/07/20 11:15:27
permalink: /article/iezlvhvg
permalink: /article/iezlvhvg/
author: pengzhanbo
top: false
type: null
---
在我们在进行基于[Vue](https://cn.vuejs.org/)的项目开发时,组件间的数据通信,是我们必须考虑的。

View File

@ -0,0 +1,211 @@
---
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

@ -0,0 +1,149 @@
---
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

@ -0,0 +1,108 @@
---
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

@ -0,0 +1,159 @@
---
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

@ -0,0 +1,227 @@
---
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

@ -0,0 +1,138 @@
---
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

@ -0,0 +1,38 @@
---
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

@ -0,0 +1,94 @@
---
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

@ -0,0 +1,176 @@
---
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

@ -0,0 +1,332 @@
---
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 安全上来带风险。

256
docs/1.前端/8.HTTP/CSP.md Normal file
View File

@ -0,0 +1,256 @@
---
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

@ -0,0 +1,90 @@
---
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

@ -0,0 +1,331 @@
---
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">
```
::: danger 警告
- 这个标签声明仅有 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。
::: danger 提醒
跟 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

@ -0,0 +1,165 @@
---
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。
```html
<script>
let count = 0;
function handleClick() {
count += 1;
}
</script>
<button on:click={handleClick}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
```

View File

@ -1,6 +1,6 @@
---
title: 面试2
createTime: 2022/04/04 01:48:00
createTime: 2022/04/03 17:48:00
author: pengzhanbo
permalink: /article/exavsmm1
---

View File

@ -2,7 +2,7 @@
title: 面试题以及个人答案 JS篇
tags:
- 面试
createTime: 2018/08/23 11:15:27
createTime: 2022/03/26 11:46:50
permalink: /article/4ml7z17g
author: pengzhanbo
top: false

View File

@ -2,7 +2,7 @@
title: 面试题以及个人答案 CSS篇
tags:
- 面试
createTime: 2018/08/22 11:15:27
createTime: 2022/03/26 11:46:50
permalink: /article/565o1wn0
author: pengzhanbo
top: false

View File

@ -1,5 +1,17 @@
---
home: true
banner: /images/big-banner.jpg
motto: 世间的美好总是不期而遇,恬静而自然。
banner: /images/bg-home.jpg
hero:
name: 鹏展博
profession: 前端开发工程师
text: 简单介绍专业技能信息相关的描述
actions:
-
theme: brand
text: Blog
link: /
-
theme: alt
text: Github
link: /
---

BIN
docs/home-banner.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

View File

@ -1,6 +1,6 @@
---
title: API
createTime: 2022/05/13 05:49:14
createTime: 2022/05/14 10:43:53
author: pengzhanbo
permalink: /note/vuepress-plugin/netlify-functions/api/
---
@ -83,4 +83,3 @@ __类型__ `{ directory: string }`
一般来说,它的值都设置为 `path.resolve(__dirname, 'functions')`

View File

@ -1,6 +1,6 @@
---
title: functions开发指南
createTime: 2022/05/13 05:45:24
createTime: 2022/05/14 10:43:53
author: pengzhanbo
permalink: /note/vuepress-plugin/netlify-functions/develop-functions/
---

View File

@ -1,6 +1,6 @@
---
title: 介绍
createTime: 2022/05/13 05:47:06
createTime: 2022/05/14 10:43:53
author: pengzhanbo
permalink: /note/vuepress-plugin/netlify-functions/intro/
---

View File

@ -1,6 +1,6 @@
---
title: 使用
createTime: 2022/05/13 05:45:01
createTime: 2022/05/14 10:43:53
author: pengzhanbo
permalink: /note/vuepress-plugin/netlify-functions/usage/
---

View File

@ -1,6 +1,6 @@
---
title: 功能
createTime: 2022/05/13 05:45:11
createTime: 2022/05/14 10:43:53
author: pengzhanbo
permalink: /note/vuepress-plugin/netlify-functions/feature/
---

View File

@ -1,6 +1,6 @@
---
title: markdown增强
createTime: 2022/04/09 06:43:32
createTime: 2022/05/14 10:43:53
author: pengzhanbo
permalink: /note/vuepress-theme-plume/markdown-enhance/
---

View File

@ -1,6 +1,6 @@
---
title: notes配置
createTime: 2022/04/09 02:48:41
createTime: 2022/05/14 10:43:53
author: pengzhanbo
permalink: /note/vuepress-theme-plume/notes-config/
---

View File

@ -1,6 +1,6 @@
---
title: 主题插件配置
createTime: 2022/04/09 02:48:30
createTime: 2022/05/14 10:43:53
author: pengzhanbo
permalink: /note/vuepress-theme-plume/plugins-config/
---

View File

@ -1,6 +1,6 @@
---
title: 主题配置
createTime: 2022/04/09 12:18:12
createTime: 2022/05/14 10:43:53
author: pengzhanbo
permalink: /note/vuepress-theme-plume/theme-config/
---

View File

@ -1,6 +1,6 @@
---
title: 基础功能
createTime: 2022/04/09 06:43:20
createTime: 2022/05/14 10:43:53
author: pengzhanbo
permalink: /note/vuepress-theme-plume/basis-power/
---

View File

@ -1,6 +1,6 @@
---
title: 快速开始
createTime: 2022/04/08 09:43:20
createTime: 2022/05/14 10:43:53
author: pengzhanbo
permalink: /note/vuepress-theme-plume/quick-start/
---

View File

@ -1,6 +1,6 @@
---
title: 编写文章
createTime: 2022/04/09 12:13:56
createTime: 2022/05/14 10:43:53
author: pengzhanbo
permalink: /note/vuepress-theme-plume/write-article/
---

View File

@ -1,6 +1,6 @@
---
title: 页面配置
createTime: 2022/04/09 01:24:17
createTime: 2022/05/14 10:43:53
author: pengzhanbo
permalink: /note/vuepress-theme-plume/page-config/
---

View File

@ -5,20 +5,20 @@
"scripts": {
"docs:build": "vuepress-cli build --clean-cache",
"docs:clean": "rimraf .vuepress/.temp .vuepress/.cache .vuepress/dist",
"docs:dev": "vuepress-cli dev --clean-cache",
"docs:dev": "vuepress-cli dev --clean-cache --clean-temp",
"docs:serve": "anywhere -s -h localhost -d .vuepress/dist"
},
"dependencies": {
"@vuepress-plume/vuepress-theme-plume": "workspace:*",
"@vuepress/bundler-vite": "2.0.0-beta.51",
"@vuepress/bundler-webpack": "2.0.0-beta.51",
"@vuepress/cli": "2.0.0-beta.51",
"@vuepress/client": "2.0.0-beta.51",
"@vuepress/utils": "2.0.0-beta.51",
"@vuepress/bundler-vite": "2.0.0-beta.60",
"@vuepress/bundler-webpack": "2.0.0-beta.60",
"@vuepress/cli": "2.0.0-beta.60",
"@vuepress/client": "2.0.0-beta.60",
"@vuepress/utils": "2.0.0-beta.60",
"anywhere": "^1.6.0",
"leancloud-storage": "^4.13.2",
"sass": "^1.55.0",
"sass-loader": "^13.1.0",
"vue": "^3.2.41"
"leancloud-storage": "^4.14.0",
"sass": "^1.57.1",
"sass-loader": "^13.2.0",
"vue": "^3.2.47"
}
}

View File

@ -33,7 +33,8 @@
"release:changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"release:check": "pnpm lint && pnpm build",
"release:publish": "pnpm -r publish",
"release:version": "bumpp package.json packages/*/package.json --execute=\"pnpm release:changelog\" --commit \"build: publish v%s\" --all --tag --push"
"release:version": "bumpp package.json packages/*/package.json --execute=\"pnpm release:changelog\" --commit \"build: publish v%s\" --all --tag --push",
"up": "taze -r major"
},
"lint-staged": {
"*.{js,ts,vue}": "eslint --fix",
@ -47,37 +48,38 @@
},
"prettier": "prettier-config-vuepress",
"devDependencies": {
"@commitlint/cli": "^17.1.2",
"@commitlint/config-conventional": "^17.1.0",
"@commitlint/cli": "^17.4.2",
"@commitlint/config-conventional": "^17.4.2",
"@types/minimist": "^1.2.2",
"@types/node": "18.8.0",
"@types/node": "18.11.18",
"@types/webpack-env": "^1.18.0",
"bumpp": "^8.2.1",
"chalk": "^5.1.2",
"commitizen": "^4.2.5",
"concurrently": "^7.4.0",
"chalk": "^5.2.0",
"commitizen": "^4.2.6",
"concurrently": "^7.6.0",
"conventional-changelog-cli": "^2.2.2",
"cpx2": "^4.2.0",
"cross-env": "^7.0.3",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^8.25.0",
"eslint-config-vuepress": "^4.0.3",
"eslint-config-vuepress-typescript": "^4.0.3",
"eslint": "^8.32.0",
"eslint-config-vuepress": "^4.0.4",
"eslint-config-vuepress-typescript": "^4.0.4",
"execa": "^6.1.0",
"handlebars": "^4.7.7",
"husky": "^8.0.1",
"lint-staged": "^13.0.3",
"husky": "^8.0.3",
"lint-staged": "^13.1.0",
"minimist": "^1.2.7",
"ora": "^6.1.2",
"pnpm": "^7.13.4",
"prettier": "^2.7.1",
"pnpm": "^7.25.0",
"prettier": "^2.8.3",
"prettier-config-vuepress": "^4.0.0",
"rimraf": "^3.0.2",
"sort-package-json": "^2.0.0",
"taze": "^0.8.2",
"sort-package-json": "^2.1.0",
"taze": "^0.8.5",
"ts-node": "^10.9.1",
"typescript": "^4.8.4",
"vite": "^3.1.8"
"tsconfig-vuepress": "^4.0.4",
"typescript": "^4.9.4",
"vite": "^4.0.4"
},
"packageManager": "pnpm@7.13.4",
"engines": {

View File

@ -0,0 +1,117 @@
# `@vuepress-plume/vuepress-plugin-auto-frontmatter`
自动生成 `*.md` 文件的 `frontmatter` 配置。
## Install
```
yarn add @vuepress-plume/vuepress-plugin-auto-frontmatter
```
## Usage
``` js
// .vuepress/config.js
import { autoFrontmatterPlugin } from '@vuepress-plume/vuepress-plugin-auto-frontmatter'
export default {
//...
plugins: [
autoFrontmatterPlugin({
formatter: {
createTime(formatTime, matter, file) {
if (formatTime) return formatTime
return file.createTime
}
}
})
]
// ...
}
```
## `autoFrontmatterPlugin([options])`
### options
`{ include?: string | string[]; exclude?: string | string[]; formatter: Formatter }`
- `include`
include 匹配字符串或数组,匹配需要自动生成 `frontmatter` 的 md文件。
默认预设为 `['**/*.md']`
- `exclude`
exclude 排除不需要的文件
默认预设为: `['!.vuepress/', '!node_modules/']`
- `formatter`
配置`frontmatter`每个字段的生成规则。
```ts
interface MarkdownFile {
filepath: string
relativePath: string
content: string
createTime: Date
}
interface FormatterFn<T = any, K = object> {
(value: T, data: K, file: MarkdownFile): T
}
type FormatterObject<K = object, T = any> = Record<
string,
FormatterFn<T, K>
>
type FormatterArray = {
include: string
formatter: FormatterObject
}[]
type Formatter = FormatterObject | FormatterArray
/**
* formatterObj 对象中的 key 即为 frontmatter 配置中的key
* 其方法返回的值将作为 frontmatter[key] 的值
* *.md
* ---
* createTime: 2022-03-26T11:46:50.000Z
* ---
*/
const formatterObj: Formatter = {
createTime(formatTime, matter, file) {
if (formatTime) return formatTime
return file.createTime
}
}
const formatterArr: Formatter = [
{
// 更精细化的匹配某个 md文件支持glob 匹配字符串
include: '**/{README,index}.md',
// formatter 仅对 glob命中的文件有效
formatter: {
home(value, matter, file) {
return value
}
},
{
// 通配如果文件没有被其他精细glob命中
// 则使用 通配 formatter
// 如果是数组,必须有且用一个 include 为 * 的 项
include: '*',
formatter: {
title(title) {
return title || '默认标题'
}
}
}
}
]
```
## Why ?
- **为什么需要这个插件?**
有时候在开发一些主题时,期望使用户更专注于内容的编写,尽可能减少配置性的工作,可以将一些重复性的必要的配置
直接通过本插件自动生成。
以及,我确实想在写新文章的时候,更少的做配置工作,于是便有了这个插件

View File

@ -0,0 +1,49 @@
{
"name": "@vuepress-plume/vuepress-plugin-auto-frontmatter",
"version": "1.0.0-beta.45",
"description": "The Plugin for VuePres 2",
"homepage": "https://github.com/pengzhanbo/vuepress-theme-plume#readme",
"bugs": {
"url": "https://github.com/pengzhanbo/vuepress-theme-plume/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/pengzhanbo/vuepress-theme-plume.git"
},
"license": "MIT",
"author": "pengzhanbo <volodymyr@foxmail.com>",
"type": "module",
"exports": {
".": "./lib/node/index.js",
"./package.json": "./package.json"
},
"main": "lib/node/index.js",
"types": "./lib/node/index.d.ts",
"files": [
"lib"
],
"scripts": {
"build": "pnpm run clean && pnpm run copy && pnpm run ts",
"clean": "rimraf lib *.tsbuildinfo",
"copy": "cpx \"src/**/*.{d.ts,vue,css,scss,jpg,png}\" lib",
"ts": "tsc -b tsconfig.build.json"
},
"dependencies": {
"@vuepress/core": "2.0.0-beta.60",
"@vuepress/shared": "2.0.0-beta.60",
"chokidar": "^3.5.3",
"create-filter": "^1.0.0",
"fast-glob": "^3.2.12",
"gray-matter": "^4.0.3",
"json2yaml": "^1.1.0"
},
"publishConfig": {
"access": "public"
},
"keyword": [
"VuePress",
"vuepress plugin",
"autoFrontmatter",
"vuepress-plugin-plugin-auto-frontmatter"
]
}

View File

@ -0,0 +1,8 @@
import type { AutoFrontmatterOptions } from '../shared/index.js'
import { autoFrontmatterPlugin } from './plugin.js'
export * from './plugin.js'
export { AutoFrontmatterOptions }
export default autoFrontmatterPlugin

View File

@ -0,0 +1,85 @@
import fs from 'node:fs'
import type { Plugin } from '@vuepress/core'
import chokidar from 'chokidar'
import { createFilter } from 'create-filter'
import grayMatter from 'gray-matter'
import jsonToYaml from 'json2yaml'
import type {
AutoFrontmatterOptions,
FormatterArray,
FormatterObject,
MarkdownFile,
} from '../shared/index.js'
import { readMarkdown, readMarkdownList } from './readFiles.js'
import { ensureArray } from './utils.js'
export const autoFrontmatterPlugin = ({
include = ['**/*.{md,MD}'],
exclude = ['.vuepress/**/*', 'node_modules'],
formatter = {},
}: AutoFrontmatterOptions = {}): Plugin => {
include = ensureArray(include)
exclude = ensureArray(exclude)
const globFilter = createFilter(include, exclude, { resolve: false })
const matterFormatter: FormatterArray = Array.isArray(formatter)
? formatter
: [{ include: '*', formatter }]
const globFormatter: FormatterObject =
matterFormatter.find(({ include }) => include === '*')?.formatter || {}
const otherFormatters = matterFormatter
.filter(({ include }) => include !== '*')
.map(({ include, formatter }) => {
return {
include,
filter: createFilter(ensureArray(include)),
formatter,
}
})
function formatMarkdown(file: MarkdownFile): void {
const { filepath, relativePath } = file
const formatter =
otherFormatters.find(({ filter }) => filter(relativePath))?.formatter ||
globFormatter
const { data, content } = grayMatter(file.content)
Object.keys(formatter).forEach((key) => {
const value = formatter[key](data[key], data, file)
data[key] = value ?? data[key]
})
const yaml = jsonToYaml
.stringify(data)
.replace(/\n\s{2}/g, '\n')
.replace(/"/g, '')
const newContent = `${yaml}---\n${content}`
fs.writeFileSync(filepath, newContent, 'utf-8')
}
return {
name: '@vuepress-plume/vuepress-plugin-auto-frontmatter',
onInitialized: async (app) => {
const markdownList = await readMarkdownList(app.dir.source(), globFilter)
markdownList.forEach((file) => formatMarkdown(file))
},
onWatched: async (app, watchers) => {
const watcher = chokidar.watch('**/*.md', {
cwd: app.dir.source(),
ignoreInitial: true,
ignored: /(node_modules|\.vuepress)\//,
})
watcher.on('add', (relativePath) => {
if (!globFilter(relativePath)) return
formatMarkdown(readMarkdown(app.dir.source(), relativePath))
})
watchers.push(watcher)
},
}
}

View File

@ -0,0 +1,37 @@
import fs from 'node:fs'
import path from 'node:path'
import fg from 'fast-glob'
import type { MarkdownFile } from '../shared/index.js'
type MarkdownFileList = MarkdownFile[]
export const readMarkdownList = async (
sourceDir: string,
filter: (id: string) => boolean
): Promise<MarkdownFileList> => {
const files: string[] = await fg(['**/*.md'], {
cwd: sourceDir,
ignore: ['node_modules', '.vuepress'],
})
return files
.filter((file) => filter(file))
.map((file) => readMarkdown(sourceDir, file))
}
export const readMarkdown = (
sourceDir: string,
relativePath: string
): MarkdownFile => {
const filepath = path.join(sourceDir, relativePath)
return {
filepath,
relativePath,
content: fs.readFileSync(filepath, 'utf-8'),
createTime: getFileCreateTime(fs.statSync(filepath)),
}
}
export const getFileCreateTime = (stat: fs.Stats): Date => {
return stat.birthtime.getFullYear() !== 1970 ? stat.birthtime : stat.atime
}

View File

@ -0,0 +1,5 @@
export function ensureArray<T>(thing: T | T[] | null | undefined): T[] {
if (Array.isArray(thing)) return thing
if (thing === null || thing === undefined) return []
return [thing]
}

View File

@ -0,0 +1,38 @@
export interface MarkdownFile {
filepath: string
relativePath: string
content: string
createTime: Date
}
export interface FormatterFn<T = any, K = object> {
(value: T, data: K, file: MarkdownFile): T
}
export type FormatterObject<K = object, T = any> = Record<
string,
FormatterFn<T, K>
>
export type FormatterArray = {
include: string
formatter: FormatterObject
}[]
export interface AutoFrontmatterOptions {
/**
* FilterPattern
*/
include?: string | string[]
exclude?: string | string[]
/**
* {
* key(value, data, file) {
* return value
* }
* }
*/
formatter?: FormatterObject | FormatterArray
}

View File

@ -0,0 +1,9 @@
{
"extends": "../tsconfig.build.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib"
},
"include": ["./src"],
"files": []
}

View File

@ -29,10 +29,10 @@
"ts": "tsc -b tsconfig.build.json"
},
"dependencies": {
"@vuepress/client": "2.0.0-beta.51",
"@vuepress/core": "2.0.0-beta.51",
"@vuepress/shared": "2.0.0-beta.51",
"@vuepress/utils": "2.0.0-beta.51"
"@vuepress/client": "2.0.0-beta.60",
"@vuepress/core": "2.0.0-beta.60",
"@vuepress/shared": "2.0.0-beta.60",
"@vuepress/utils": "2.0.0-beta.60"
},
"publishConfig": {
"access": "public"

View File

@ -2,7 +2,8 @@
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib"
"outDir": "./lib",
"composite": true
},
"include": ["./src"],
"files": []

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (C) 2021 - PRESENT by pengzhanbo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,51 @@
{
"name": "@vuepress-plume/vuepress-plugin-blog-data",
"version": "1.0.0-beta.45",
"description": "The Plugin for VuePres 2",
"homepage": "https://github.com/pengzhanbo/vuepress-theme-plume#readme",
"bugs": {
"url": "https://github.com/pengzhanbo/vuepress-theme-plume/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/pengzhanbo/vuepress-theme-plume.git"
},
"license": "MIT",
"author": "pengzhanbo <volodymyr@foxmail.com>",
"type": "module",
"exports": {
".": "./lib/node/index.js",
"./client": "./lib/client/index.js",
"./package.json": "./package.json"
},
"main": "lib/node/index.js",
"types": "./lib/node/index.d.ts",
"files": [
"lib"
],
"scripts": {
"build": "pnpm run clean && pnpm run copy && pnpm run ts",
"clean": "rimraf lib *.tsbuildinfo",
"copy": "cpx \"src/**/*.{d.ts,vue,css,scss,jpg,png}\" lib",
"ts": "tsc -b tsconfig.build.json"
},
"dependencies": {
"@vue/devtools-api": "^6.4.5",
"@vuepress/client": "2.0.0-beta.60",
"@vuepress/core": "2.0.0-beta.60",
"@vuepress/shared": "2.0.0-beta.60",
"@vuepress/utils": "2.0.0-beta.60",
"chokidar": "^3.5.3",
"create-filter": "^1.0.0",
"vue": "^3.2.47"
},
"publishConfig": {
"access": "public"
},
"keyword": [
"VuePress",
"vuepress plugin",
"blogData",
"vuepress-plugin-plugin-blog-data"
]
}

View File

@ -0,0 +1,7 @@
import type { BlogPostData } from '../shared/index.js'
declare module '@internal/blogData' {
const blogPostData: BlogPostData
export { blogPostData }
}

View File

@ -0,0 +1 @@
export * from './useBlogPostData.js'

View File

@ -0,0 +1,20 @@
import { blogPostData as blogPostDataRaw } from '@internal/blogData'
import { ref } from 'vue'
import type { Ref } from 'vue'
import type { BlogPostData } from '../../shared/index.js'
declare const __VUE_HMR_RUNTIME__: Record<string, any>
export type BlogDataRef<T extends BlogPostData = BlogPostData> = Ref<T>
export const blogPostData: BlogDataRef = ref(blogPostDataRaw)
export const useBlogPostData = <
T extends BlogPostData = BlogPostData
>(): BlogDataRef<T> => blogPostData as BlogDataRef<T>
if (import.meta.webpackHot || import.meta.hot) {
__VUE_HMR_RUNTIME__.updateBlogData = (data: BlogPostData) => {
blogPostData.value = data
}
}

View File

@ -0,0 +1,37 @@
import { setupDevtoolsPlugin } from '@vue/devtools-api'
import { defineClientConfig } from '@vuepress/client'
import { useBlogPostData } from './composables/index.js'
declare const __VUE_PROD_DEVTOOLS__: boolean
export default defineClientConfig({
enhance({ app }) {
const blogPostData = useBlogPostData()
// setup devtools in dev mode
if (__VUEPRESS_DEV__ || __VUE_PROD_DEVTOOLS__) {
setupDevtoolsPlugin(
{
// fix recursive reference
app: app as any,
id: 'org.vuepress-plume.plugin-blog-data',
label: 'VuePress Blog Data Plugin',
packageName: '@vuepress/plugin-blog-data',
homepage: 'https://pengzhanbo.cn',
logo: 'https://v2.vuepress.vuejs.org/images/hero.png',
componentStateTypes: ['VuePress'],
},
(api) => {
api.on.inspectComponent((payload) => {
payload.instanceData.state.push({
type: 'VuePress',
key: 'blogPostData',
editable: false,
value: blogPostData.value,
})
})
}
)
}
},
})

View File

@ -0,0 +1,4 @@
import type { BlogPostData, BlogPostDataItem } from '../shared/index.js'
export * from './composables/index.js'
export { BlogPostData, BlogPostDataItem }

View File

@ -0,0 +1,6 @@
import { blogDataPlugin } from './plugin.js'
export * from '../shared/index.js'
export { blogDataPlugin }
export default blogDataPlugin

View File

@ -0,0 +1,58 @@
import type { Plugin } from '@vuepress/core'
import { getDirname, path } from '@vuepress/utils'
import chokidar from 'chokidar'
import { createFilter } from 'create-filter'
import { preparedBlogData } from './prepareBlogData.js'
import type { BlogDataPluginOptions } from './index.js'
const __dirname = getDirname(import.meta.url)
export type PluginOption = Omit<BlogDataPluginOptions, 'include' | 'exclude'>
export const blogDataPlugin = ({
include,
exclude,
...pluginOptions
}: BlogDataPluginOptions = {}): Plugin => {
const pageFilter = createFilter(toArray(include), toArray(exclude), {
resolve: false,
})
return {
name: '@vuepress-plume/vuepress-plugin-blog-data',
clientConfigFile: path.resolve(__dirname, '../client/config.js'),
extendsPage(page) {
if (page.filePathRelative && pageFilter(page.filePathRelative)) {
;(page.data as any).isBlogPost = true
}
},
onPrepared: async (app) =>
await preparedBlogData(app, pageFilter, pluginOptions),
onWatched(app, watchers) {
const watcher = chokidar.watch('pages/**/*', {
cwd: app.dir.temp(),
ignoreInitial: true,
})
watcher.on(
'add',
async () => await preparedBlogData(app, pageFilter, pluginOptions)
)
watcher.on(
'change',
async () => await preparedBlogData(app, pageFilter, pluginOptions)
)
watcher.on(
'unlink',
async () => await preparedBlogData(app, pageFilter, pluginOptions)
)
watchers.push(watcher)
},
}
}
function toArray(likeArr: string | string[] | undefined): string[] {
if (Array.isArray(likeArr)) return likeArr
return likeArr ? [likeArr] : []
}

View File

@ -0,0 +1,81 @@
import type { App, Page } from '@vuepress/core'
import type { BlogPostData, BlogPostDataItem } from '../shared/index.js'
import type { PluginOption } from './plugin.js'
const HMR_CODE = `
if (import.meta.webpackHot) {
import.meta.webpackHot.accept()
if (__VUE_HMR_RUNTIME__.updateBlogData) {
__VUE_HMR_RUNTIME__.updateBlogData(blogPostData)
}
}
if (import.meta.hot) {
import.meta.hot.accept(({ blogPostData }) => {
__VUE_HMR_RUNTIME__.updateBlogData(blogPostData)
})
}
`
const getTimestamp = (time: Date): number => {
return new Date(time).getTime()
}
const EXCERPT_SPLIT = '<!-- more -->'
export const preparedBlogData = async (
app: App,
pageFilter: (id: string) => boolean,
options: PluginOption
): Promise<void> => {
let pages = app.pages.filter((page) => {
return page.filePathRelative && pageFilter(page.filePathRelative)
})
if (options.sortBy) {
pages = pages.sort((prev, next) => {
if (options.sortBy === 'createTime') {
return getTimestamp(prev.frontmatter.createTime as Date) <
getTimestamp(next.frontmatter.createTime as Date)
? 1
: -1
} else {
return typeof options.sortBy === 'function' &&
options.sortBy(prev, next)
? 1
: -1
}
})
}
const blogData: BlogPostData = pages.map((page: Page) => {
let extended: Partial<BlogPostDataItem> = {}
if (typeof options.extendBlogData === 'function') {
extended = options.extendBlogData(page)
}
const data = {
path: page.path,
title: page.title,
...extended,
}
if (options.excerpt && page.contentRendered.includes(EXCERPT_SPLIT)) {
const contents = page.contentRendered.split(EXCERPT_SPLIT)
data.excerpt = contents[0]
}
return data as BlogPostDataItem
})
let content = `\
export const blogPostData = JSON.parse(${JSON.stringify(
JSON.stringify(blogData)
)})
`
// inject HMR code
if (app.env.isDev) {
content += HMR_CODE
}
await app.writeTemp('internal/blogData.js', content)
}

View File

@ -0,0 +1,16 @@
export interface BlogDataPluginOptions {
include?: string | string[]
exclude?: string | string[]
sortBy?: 'createTime' | false | (<T>(prev: T, next: T) => boolean)
excerpt?: boolean
extendBlogData?: <T = any>(page: T) => Record<string, any>
}
export type BlogPostData<T extends object = object> = BlogPostDataItem<T>[]
export type BlogPostDataItem<T extends object = object> = {
path: string
title: string
excerpt: string
[x: string]: any
} & T

View File

@ -0,0 +1,13 @@
{
"extends": "../tsconfig.build.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"baseUrl": ".",
"paths": {
"@internal/blogData": ["./src/client/blogPostData.d.ts"]
},
"types": ["@vuepress/client/types", "vite/client", "webpack-env"]
},
"include": ["./src"]
}

View File

@ -37,10 +37,10 @@
},
"dependencies": {
"@types/markdown-it": "^12.2.3",
"@vuepress/cli": "2.0.0-beta.51",
"@vuepress/client": "2.0.0-beta.51",
"@vuepress/core": "2.0.0-beta.51",
"@vuepress/utils": "2.0.0-beta.51",
"@vuepress/cli": "2.0.0-beta.60",
"@vuepress/client": "2.0.0-beta.60",
"@vuepress/core": "2.0.0-beta.60",
"@vuepress/utils": "2.0.0-beta.60",
"markdown-it-container": "^3.0.0"
},
"publishConfig": {

View File

@ -30,12 +30,12 @@
"ts": "tsc -b tsconfig.build.json"
},
"dependencies": {
"@vuepress/client": "2.0.0-beta.51",
"@vuepress/core": "2.0.0-beta.51",
"@vuepress/shared": "2.0.0-beta.51",
"@vuepress/utils": "2.0.0-beta.51",
"vue": "^3.2.41",
"vue-router": "4.1.5"
"@vuepress/client": "2.0.0-beta.60",
"@vuepress/core": "2.0.0-beta.60",
"@vuepress/shared": "2.0.0-beta.60",
"@vuepress/utils": "2.0.0-beta.60",
"vue": "^3.2.47",
"vue-router": "4.1.6"
},
"publishConfig": {
"access": "public"

View File

@ -38,21 +38,21 @@
},
"dependencies": {
"@iarna/toml": "^2.2.5",
"@netlify/functions": "^1.3.0",
"@vuepress/core": "2.0.0-beta.51",
"@vuepress/shared": "2.0.0-beta.51",
"@vuepress/utils": "2.0.0-beta.51",
"chalk": "^5.1.2",
"@netlify/functions": "^1.4.0",
"@vuepress/core": "2.0.0-beta.60",
"@vuepress/shared": "2.0.0-beta.60",
"@vuepress/utils": "2.0.0-beta.60",
"chalk": "^5.2.0",
"chokidar": "^3.5.3",
"cpx2": "^4.2.0",
"dotenv": "^16.0.3",
"esbuild": "^0.15.11",
"esbuild": "^0.16.17",
"execa": "^6.1.0",
"netlify-cli": "^12.0.9",
"netlify-cli": "^12.7.2",
"portfinder": "^1.0.32"
},
"devDependencies": {
"@types/node": "^18.8.5"
"@types/node": "^18.11.18"
},
"publishConfig": {
"access": "public"

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (C) 2021 - PRESENT by pengzhanbo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,18 @@
# `@vuepress-plume/vuepress-plugin-notes-data`
## Install
```
yarn add @vuepress-plume/vuepress-plugin-notes-data
```
## Usage
``` js
// .vuepress/config.js
const notesDataPlugin = require('@vuepress-plume/vuepress-plugin-notes-data')
module.exports = {
//...
plugins: [
notesDataPlugin()
]
// ...
}
```

View File

@ -0,0 +1,51 @@
{
"name": "@vuepress-plume/vuepress-plugin-notes-data",
"version": "1.0.0-beta.45",
"description": "The Plugin for VuePres 2",
"homepage": "https://github.com/pengzhanbo/vuepress-theme-plume#readme",
"bugs": {
"url": "https://github.com/pengzhanbo/vuepress-theme-plume/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/pengzhanbo/vuepress-theme-plume.git"
},
"license": "MIT",
"author": "pengzhanbo <volodymyr@foxmail.com>",
"type": "module",
"exports": {
".": "./lib/node/index.js",
"./client": "./lib/client/index.js",
"./package.json": "./package.json"
},
"main": "lib/node/index.js",
"types": "./lib/node/index.d.ts",
"scripts": {
"build": "pnpm run clean && pnpm run copy && pnpm run ts",
"clean": "rimraf lib *.tsbuildinfo",
"copy": "cpx \"src/**/*.{d.ts,vue,css,scss,jpg,png}\" lib",
"copy:watch": "cpx \"src/**/*.{d.ts,vue,css,scss,jpg,png}\" lib -w",
"dev": "concurrently \"pnpm copy:watch\" \"pnpm ts:watch\"",
"ts": "tsc -b tsconfig.build.json",
"ts:watch": "tsc -b tsconfig.build.json --watch"
},
"dependencies": {
"@vue/devtools-api": "^6.4.5",
"@vuepress/client": "2.0.0-beta.60",
"@vuepress/core": "2.0.0-beta.60",
"@vuepress/shared": "2.0.0-beta.60",
"@vuepress/utils": "2.0.0-beta.60",
"chokidar": "^3.5.3",
"create-filter": "^1.0.0",
"vue": "^3.2.47"
},
"publishConfig": {
"access": "public"
},
"keyword": [
"VuePress",
"vuepress plugin",
"notesData",
"vuepress-plugin-plugin-notes-data"
]
}

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