0%

setup只会在被挂载时执行一次,返回值由两种情况:

1 返回一个函数,该函数将作为组件的render函数

2 返回一个对象,该对象包含的数据将暴露给模板使用

setup函数第一个参数为外部为组件传递的props数据对象,第二个参数为setupContext对象,其中保存着组件接口相关的数据和方法:slots,emit,attrs,expose

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
function mountComponent(vnode,container,anchor) {
const componentOptions = vnode.type
//从组件选项中取出setup函数
let {render,data,setup} = componentOptions
beforeCreate && beforeCreate()
const state = data ? reactive(data()) : null
const [props,attrs] = resolveProps(propsOption,vnode,props)
const instance = {
state,
props:shallowReactive(props),
isMounted: false,
subTree: null
}
//setupContext包含slots,emit,attrs,expose
const setupContext = {attrs}
//setupContext作为第二个参数传入
const setupResult = setup(shallowReadOnly(instance.props),setupContext)
//如果setupResult为函数,则作为渲染函数
if(typeof setupResult === 'function'){
//冲突
if(render)console.error('setup函数返回渲染函数,render选项被忽略')
render = setupResult
}else{
//如果setup返回值不是函数,则作为数据状态赋值给setupState
setupState = setupContext
}
//渲染函数上下文对象,本质上是组件实例的代理,使得渲染函数能够通过this访问props数据和组件自身状态
const renderContext = new Proxy(instance,{
get(t,k,t){
const {state,props,slots} = t
//先尝试读取自身状态数据
if(state && k in state) {
return state[k]
}else if(k in props){
return props[k]//如果组件自身没有该数据,尝试从props上读取
}else {
console.error('不存在')
}
//当k的值为$slots,直接返回组件实例上的slots
if(k === '$slots')return slots
//...
},
set(t,k,v,r){
const [state,props] = t
if(state && k in state){
state[k]=t
}else if(k in props){
props[k]=t
}else{
console.error('不存在')
}
}
})

}
//用于解析组件props和attrs数据
function resolveProps(options,propsData) {
const props = {}
const attrs = {}
//遍历为组件传递的props数据
for(const key in propsData) {
if(key in options){
//如果为组件传递的props数据在组件自身的props选项中有定义,则将其视为合法的props
props[key] = propsData[key]
}else{
attrs[key] = propsData[key]
}
}
return [props,attrs]
}

MyComponent组件模板:

1
2
3
4
5
6
7
<template>
<header><slot name="header"></slot></header>
<div>
<slot name="body"></slot>
</div>
<footer><slot name="footer"></slot></footer>
</template>

当在父组件中使用组件时,可以根据插槽名字插入自定义的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<MyComponent>
<template #header>
<h1>
我是标题
</h1>
</template>
<template #body>
<section>
我是内容
</section>
</template>
<template #footer>
<p>
我是注脚
</p>
</template>
</MyComponent>

父组件模板会被编译成如下渲染函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function render(){
return{
type: MyComponent,
//组件的children会被编译成一个对象
children: {
header() {
return {type:'h1',children:'我是标题'}
},
body() {
return {type:'section',children:'我是内容'}
},
foote() {
return {type:'p',children:'我是注脚'}
}
}
}
}

组件模板中的插槽内容会被编译为插槽函数,而插槽函数的返回值就是具体的插槽内容

MyComponent的模板会被编译成如下渲染函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function render() {
return [
{
type: "header",
children: [this.$slots.header()]
},
{
type: 'div',
children: [this.$slots.body()]
},
{
type: 'footer',
children: [this.$slots.footer()]
}
]
}

渲染插槽内容的过程,就是调用插槽函数并渲染由其返回的内容的过程,在运行时实现上,插槽依赖于setupContext中的slots对象,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function mountComponent(vnode,container,anchor) {
//...
//直接使用编译好的vnode.children对象作为slots对象
const slots = vnode.children || {}
//将slot对象添加到setupContext中
const setupContext = {attrs,emit,slots}
const instance = {
state,
props: shallowReactive(props),
isMounnted:false,
subTree:null,
//将插槽添加到组件实例
slots
}
const renderContext = new Proxy(instance,{
get(t,k,t){
const {state,props,slots} = t
//当k的值为$slots,直接返回组件实例上的slots
if(k === '$slots')return slots
//...
},
set(t,k,v,r){
//...
}
})
}

Vue2中使用双端diff算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
//双端diff算法
function patchKeyedChildren(n1,n2,container) {
const oldChildren = n1.children
const newChildren = n2.children
//是个索引值
let oldStartIdx = 0
let oldEndIdx = oldChildren.length-1
let newStartIdx = 0
let newEndIdx = newChildren.length-1
//四个索引指向的vnode
let oldStartVnode = oldChildren[oldStartIdx]
let oldEndVnode = oldChildren[oldEndIdx]
let newStartVnode = newChildren[newStartIdx]
let newEndVnode = newChildren[newEndIdx]
//如果头尾部找不到复用的节点,只能拿新的一组子节点中的头部节点去旧的一组子节点中寻找
while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
//如果旧结点数组中头部结点或者尾部结点为undefined,说明已经被处理过了,直接跳到下一个位置
if(!oldStartVnode) {
oldStartVnode = oldChildren[++oldStartIdx]
}else if(!oldEndVnode){
oldEndVnode = oldChildren[--oldEndIdx]
}
else if(oldStartVnode.key === newStartVnode.key){
patch(oldStartVnode,newStartVnode,container)
//更新相关索引
oldStartVnode = oldChildren[++oldStartIdx]
newStartVnode = newChildren[++newStartIdx]
}//结点在新的顺序中仍处于尾部,不需要移动,打补丁后更新索引和头尾部结点变量
else if(oldEndVnode.key === newEndVnode.key){
patch(oldEndVnode,newEndVnode,container)
oldEndVnode = oldChildren[--oldEndIdx]
newEndVnode = newChildren[--newEndIdx]
}else if(oldStartVnode.key === newEndVnode.key){
patch(oldStartVnode,newEndVnode,container)
//原本是头部结点,在新的顺序中变为了尾部结点,将旧结点对应的真实DOM移动到旧的一组子节点的尾部结点所对应的真实DOM后面,更新索引
insert(oldStartVnode.el,container,oldEndVnode.el.nextSibling)
oldStartVnode = oldChildren[++oldStartIdx]
newEndVnode = newChildren[--newEndIdx]

}else if(oldEndVnode.key === newStartVnode.key){
patch(oldEndVnode,newStartVnode,container)
//结点p-4原本是最后一个子节点,在新的顺序中它变成了第一个子节点,因此,将索引oldEndIdx指向的虚拟结点对应的真实DOM移动到索引oldStartIdx指向得到虚拟结点所对应的真实DOM前面
insert(oldEndVnode.el,container,oldStartVnode.el)
oldEndVnode = oldChildren[--oldEndIdx]
newStartVnode = newChildren[++newStartIdx]
}else {
//遍历旧的一组子节点,试图寻找与newStartVnode拥有相同key的节点
//idxInOld就是新的一组子节点的头部节点在旧的一组子节点中的索引
const idxInOld = oldChildren.findIndex(
node=>node.key===newStartVnode.key
)
//idxInOld大于0说明·找到了可以复用的结点,并且需要将其对应的真实DOM移动到头部
if(idxInOld > 0){
//idxInOld位置对应的vnode就是需要移动的结点
const vnodeToMove = oldChildren[idxInOld]
patch(vnodeToMove,newStartVnode,container)
//将vnodeToMove移动到头部结点oldStartVnode.el之前
insert(vnodeToMove.el,container,oldStartVnode.el)
//由于位置idxInOld处的结点所对应的真实DOM已经移动到了别处,因此将其设置为undefined
oldChildren[idxInOld] = undefined


}else{
//将newStartVnode作为新节点挂载到头部,使用当前头部结点oldStartVnode.el作为锚点
patch(null,newStartVnode,container,oldStartVnode.el)
}
//更新newStartIdx
newStartVnode = newChildren[++newStartIdx]
}
}
//如果oldStartIdx已经大于oldEndIdx,但是newStartIdx<=newEndIdx,说明新节点中还有元素未被挂载,需要挂载它们
if(oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx){
for(let i = newStartIdx; i<=newEndIdx ; i++){
patch(null,newChildren[i],container,oldStartVnode.el)
}
}
//如果newStartIdx已经大于newEndIdx,而oldStartIdx小于等于newEndIdx,则旧的结点中有结点需要移除
if(newEndIdx < newStartIdx && oldStartIdx<=oldEndIdx) {
for(let i=oldStartIdx;i<oldEndIdx;i++){
unmount(oldChildren[i])
}
}

}
function insert(el,parent,anchor=null){
parent.insertBefore(el,anchor)
}

Vue3快速diff算法

在实测中性能最优,它借鉴了文本Diff中的预处理思路,先处理新旧两组结点中相同的前置结点和相同的后置结点,当前前置结点和后置结点全部处理完毕后,如果无法简单地通过挂载新节点或者卸载已经不存在的结点来完成更新,则需要根据结点的索引关系,构造一个最长递增子序列,最长递增子序列所指向的结点即为不需要移动的结点,构造source数组,用来存储新的一组子节点在旧子节点中的位置索引,后面用它来计算最长递增子序列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
function patchKeyedChildren(n1,n2,container) {
//更新相同的前置结点
let j = 0;
let oldVnode = oldChildren[j]
let newVnode = newChildren[j]
while(oldVnode.key === newVnode.key) {
patch(oldVnode,newVnode,container)
j++
oldVnode = oldChildren[j]
newVnode = newChildren[j]
}
//更细相同的后置结点
//索引oldEnd指向旧的一组子节点的最后一个结点
let oldEnd = oldChildren.length-1
let newEnd = newChildren.length-1
oldVnode = oldChildren[oldEnd]
newVnode = newChildren[newEnd]
//while循环从后向前遍历,直到遇到拥有不同key值的结点为止
while(oldVnode.key === newVnode.key){
patch(oldVnode,newVnode,container)
oldEnd--;
newEnd--;
oldVnode=oldChildren[oldEnd]
newVnode=newChildren[newEnd]
}
//预处理完毕,如果j>oldEnd并且j<=newEnd,说明从j到newEnd之间的结点应该作为新节点插入
if(j>oldEnd && j<=newEnd) {
//锚点索引
const anchorIndex = newEnd+1
//锚点元素
const anchor = anchorIndex < newChildren.length ? newChildren[anchorIndex].el :null
//挂载新节点
while(j<newEnd){
patch(null,newChildren[j++],container,anchor)
}
}else if(j>newEnd&&j<=oldEnd){
//j到oldEnd之间的结点应该被卸载
while(j<=oldEnd){
unmount(oldChildren[j++])
}
}else{
//构造source数组,用来存储新的一组子节点在旧子节点中的位置索引,后面用它来计算最长递增子序列
const count = newEnd -j+1
const source =newArray(count)
source.fill(-1)
//新增pos和move用来判断结点是否需要移动
let move =false
let pos =0
//oldStart和newStart分别为起始索引,即j
const oldStart = j
const newStart = j
//构建索引表,存储新子节点数组中键值和索引
const keyIndex = {}
for(let i = newStart;i<=newEnd;i++){
keyIndex[newChildren[i].key]=i
}
//新增patched代表更新过的结点数量
let patched = 0;
//遍历旧的一组子节点

for(let i = oldStart;i<=oldEnd;i++){
const oldVnode = oldChildren[i]
//更新过的结点数量小于等于需要更新的结点数量,执行更新
if(patched<=count){
//通过索引表快速找到新子节点中和旧子节点有相同key的结点位置
const k= keyIndex[oldVnode.key]
if(typeof ke !== "undefined"){
newVnode = newChildren[k]
//调用patch函数完成更新
patch(oldVnode,newVnode,container)
patched++;
//填充source数组
source[k-newStart]=i
if(k<pos){
move=true
}else{
pos=k;
}
}else{
unmount(oldVnode)
}

}else{//更新过的结点数量大于需要更新的结点数量,则卸载多于的结点
unmount(oldVnode)

}
}
if(move){
//如果move为真,则需要进行DOM移动操作
const seq = lis(sources)//[0,1]计算最长递增子序列的索引信息
//s指向最长递增子序列的最后一个元素
let s=seq.length-1
let i=count-1
for(i;i>=0;i--){
if(source[i]===-1){
//说明索引为i的结点为全新的结点,挂载
//该结点在新子节点中的索引
const pos=i+newStart
const newVnode = newChildren[pos]
//该结点的下一个结点的位置索引
const newPos=pos+1
//锚点
const anchor = newPos <newChildren.length?newChildren[newPos].el:null
//挂载
patch(null,newVnode,container,anchor)
}else if(i!==seq[s]){//说明节点需要移动
//该结点在新子节点中的索引
const pos=i+newStart
const newVnode = newChildren[pos]
//该结点的下一个结点的位置索引
const newPos=pos+1
//锚点
const anchor = newPos <newChildren.length?newChildren[newPos].el:null
//移动
insert(newVnode.el,container,anchor)
}else{
//当i===seq[j],说明该位置结点不需要移动,让s指向下一个位置
s--
}
}

}

}

}
function lis(arr) {
let len = arr.length,
res = [],
dp = new Array(len).fill(1);
// 存默认index
for (let i = 0; i < len; i++) {
res.push([i])
}
for (let i = len - 1; i >= 0; i--) {
let cur = arr[i],
nextIndex = undefined;
// 如果为-1 直接跳过,因为-1代表的是新节点,不需要进行排序
if (cur === -1) continue
for (let j = i + 1; j < len; j++) {
let next = arr[j]
// 满足递增条件
if (cur < next) {
let max = dp[j] + 1
// 当前长度是否比原本的长度要大
if (max > dp[i]) {
dp[i] = max
nextIndex = j
}
}
}
// 记录满足条件的值,对应在数组中的index
if (nextIndex !== undefined) res[i].push(...res[nextIndex])
}
let index = dp.reduce((prev, cur, i, arr) => cur > arr[prev] ? i : prev, dp.length - 1)
// 返回最长的递增子序列的index
return result[index]
}

前言:

搭建属于自己的组件库,方便在项目中导入组件,避免重复造轮子。

搭建组件库的主要步骤:

  • 项目初始化:使用eslint/commit lint/typeScript进行代码规范
  • 文档编写:使用dumi1进行开发调试以及文档编写
  • 打包阶段:输出umd/cjs/esm产物并支持按需加载
  • 组件测试:使用jest库,@test-library/react相关生态进行组件测试
  • 发布npm:编写脚本完成发布
  • 部署文档站点:使用github pages以及github actions完成文档站点自动部署

技术栈:

组件编写:react,typescript

代码规范:eslint,prettier

打包编译:gulp,babel

文档编写:dumi

样式处理:scss

整个项目结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
.github
-- workflows
-- !gh-pages.yml//master触发push操作时则会自动站点部署
doc-site//部署文档
-- demos
-- alert
-- alert__index.md.chunk.css
-- ...
esm
--alert
-- __tests__
-- index.test.js
-- style
-- css.js
-- index.css
-- index.d.ts
-- index.js
-- index.scss
-- index.d.ts
-- index.js
lib
--alert
-- __tests__
-- index.test.js
-- style
-- css.js
-- index.css
-- index.d.ts
-- index.js
-- index.scss
-- index.d.ts
-- index.js
--node_modules
--src//组件源码
-- .umi
--.umi-production
--alert
-- __tests__
-- index.test.tsx
--demo
--basic.tsx
-- style
-- index.css
-- index.ts
-- index.scss
-- index.md
-- index.tsx
--alert.js
--.babelrc.js//用于编译代码
--browerserslistrc//设置兼容浏览器
--commitlintrc.js//规范commit message
--.stylelintrc.js//规范样式代码
--.umirc.ts
--eslintrc.js//规范代码
--gatsby-config.js
--gulpfile.js//代码打包
--jest.config.js//配置测试
--prettierrc.js//规范代码格式
--package.json
--package.lock.json
--tsconfig.build.json
--tsconfig.json




初始化项目:

1
2
3
4
mkdir my-ui
cd my-ui
npm init --y
mkdir src && cd src && touch index.ts//新建源码文件以及入口文件

代码规范:

直接使用@umijs.fabric的配置

1
npm i --dev @umi/fabric prettier

.eslintrc.js:

1
2
3
module.exports = {
extends: [require.resolve('@umijs/fabric/dist/eslint')],
};

.prettierrc.js:

1
2
3
4
5
const fabric = require('@umijs/fabric');

module.exports = {
...fabric.prettier,
};

.stylelintrc.js:

1
2
3
module.exports = {
extends: [require.resolve('@umijs/fabric/dist/stylelint')],
};

关于eslint和prettier的配置:

使用ESlint+Prettier规范React+TypeScript项目:https://zhuanlan.zhihu.com/p/62401626

代码规范检测需要用husky:

1
npm i husky lint-staged --dev

commit message检测:

1
npm i @commitlint/cli @commitlint/config-conventional commitizen cz-conventional-changelog --dev

新增.commitlintrc.js写入一下内容:

1
module.exports = { extends: ['@commitlint/config-conventional'] };

package.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//...
"scripts": {
"commit": "git-cz",
}
//...
"lint-staged": {
"src/**/*.ts?(x)": [
"prettier --write",
"eslint --fix",
"git add"
],
"src/**/*.less": [
"stylelint --syntax less --fix",
"git add"
]
},
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
"pre-commit": "lint-staged"
}
}
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
}

后面使用npm run commit代替git commit生成规范的commit message,也可以手写,但是要符合规范

配置typeScript:

1
npm i typescript --dev

新建tsconfig.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"compilerOptions": {
"baseUrl": "./",
"target": "esnext",
"module": "commonjs",
"jsx": "react",
"declaration": true,
"declarationDir": "lib",
"strict": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"resolveJsonModule": true
},
"include": ["src", "typings.d.ts"],
"exclude": ["node_modules"]
}

编写组件代码:

1
2
3
4
5
alert
├── index.tsx # 源文件
└── style
├── index.less # 样式文件
└── index.ts # 样式文件里为什么存在一个index.ts - 按需加载样式 管理样式依赖 后面章节会提到

下载react相关依赖:

1
2
3
npm i react react-dom @types/react @types/react-dom --dev # 开发时依赖,宿主环境一定存在

npm i prop-types # 运行时依赖,宿主环境可能不存在 安装本组件库时一起安装

组件文档:

用dumi作为组件文档站点工具,并兼具开发调试功能

1
npm i dumi serve --dev

增加npm scripts到package.json

1
2
3
4
5
"scripts": {
"dev": "dumi dev", // 启动开发环境 在文档站点中调试组件
"build:site": "rimraf doc-site && dumi build", // 构建文档站点 后续会部署到 github pages
"preview:site": "npm run build:site && serve doc-site" // 本地预览构建后的文档站点
},

配置.umirc.ts配置文件:

1
2
3
4
5
6
7
8
9
import { defineConfig } from 'dumi';

export default defineConfig({
title: 'My UI', // 站点名称
mode: 'site',
outputPath: 'doc-site', // 输出文件夹
exportStatic: {}, // 后续会部署到 github pages 直接全部生成静态页面 不走前端路由
dynamicImport: {}, // 拆包 站点过大时可以优化首屏加载速度
});

首页配置:

docs/index.md:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
---
title: My UI
hero:
title: My UI
desc: 文档站点基于 dumi 生成
actions:
- text: 快速上手
link: /getting-started
features:
- icon: https://gw.alipayobjects.com/zos/bmw-prod/881dc458-f20b-407b-947a-95104b5ec82b/k79dm8ih_w144_h144.png
title: 特性 1
desc: Balabala
- icon: https://gw.alipayobjects.com/zos/bmw-prod/d60657df-0822-4631-9d7c-e7a869c2f21c/k79dmz3q_w126_h126.png
title: 特性 2
desc: Balabala
- icon: https://gw.alipayobjects.com/zos/bmw-prod/d1ee0c6f-5aed-4a45-a507-339a4bfe076c/k7bjsocq_w144_h144.png
title: 特性 3
desc: Balabala
footer: Open-source MIT Licensed | Copyright © 2020<br />Powered by [dumi](https://d.umijs.org)
---

可以参考dumi文档进行配置

index.md:

1
2
3
4
5
6
7
8
9
10
-- __tests__
-- index.test.js
-- style
-- css.js
-- index.css
-- index.d.ts
-- index.js
-- index.less
-- index.d.ts
-- index.js

部署文档站点:

将文档部署到github pages

下载cross–env区分环境变量

1
npm i cross-env --dev

package.json:

1
2
3
"scripts": {
"preview:site": "cross-env SITE_BUILD_ENV=PREVIEW npm run build:site && serve doc-site",
},

.umirc.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { defineConfig } from 'dumi';
let base = '/my-react-components';
let publicPath = '/my-react-components/';
if(process.env.SITE_BUID_ENV === 'PREVIEW'){
base = undefined;
publicPath = undefined;
}
export default defineConfig({
title: 'My UI', // 站点名称
mode: 'site',
outputPath: 'doc-site', // 输出文件夹
exportStatic: {}, // 后续会部署到 github pages 直接全部生成静态页面 不走前端路由
dynamicImport: {}, // 拆包 站点过大时可以优化首屏加载速度
base,
publicPath,
});

下载gh-pages完成一键部署:

1
npm i gh-pages --dev

package.json:

1
2
3
"scripts":{
"deploy:site":"npm run build:site && gh-pages -d doc-site",
}

执行npm run deploy:site后就能在${username}.github.io/${repo}看到自己的组件库站点

使用Github Actions自动触发部署:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
name: github pages

on:
push:
branches:
- master # default branch

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: npm install
- run: npm run build:site
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./doc-site

编译打包:

1.导出类型声明文件:

先下载cpr,这样运行下面的npm run build:typs时会将lib的声明文件拷贝一份,并将文件夹重命名为esm,后面存放ES module形式的组件,这样做的原因是保证用户手动按需引入组件的时候可以获取自动提示

**使用typescript编写的组件库,应该利用类型系统的好处,我们可以生成类型声明文件,**并在package.json中定义入口:

1
2
3
4
5
6
{
"typings": "lib/index.d.ts", // 定义类型入口文件
"scripts": {
"build:types": "tsc -p tsconfig.build.json && cpr lib esm" // 执行tsc命令生成类型声明文件
}
}

配置tsconfig.build.json:

1
2
3
4
5
{
"extends": "./tsconfig.json",
"compilerOptions": { "emitDeclarationOnly": true }, // 只生成声明文件
"exclude": ["**/__tests__/**", "**/demo/**", "node_modules", "lib", "esm"] // 排除示例、测试以及打包好的文件夹
}

执行npm run build:types后根目录生成了lib文件夹(tsconfig.json中定义的declarationDir字段)以及esm文件夹(拷贝而来),目录结构与src文件夹保持一致,这样使用者引入npm包时,便能得到自动提示,也能复用相关组件的类型定义**

接下来将ts(x)等文件处理成js文件,需要配置.babelrc.js,因为我们需要输出commonjs module和es module,再者,考虑到样式处理及其按需加载,用gulp

配置目标环境:

为了避免转译浏览器原生支持多的语法,新建.browserslistrc文件,根据适配需求,写入支持浏览器范围,作用于@babel/preset-env

.browserslistrc

1
2
3
>0.2%
not dead
not op_mini all

babel配置:

1
npm i @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript @babel/plugin-proposal-class-properties  @babel/plugin-transform-runtime  @babel/runtime-corejs3 --dev

.babelrc.js:

1
2
3
4
module.exports = {
presets: ['@babel/env', '@babel/typescript', '@babel/react'],
plugins: ['@babel/plugin-transform-runtime', '@babel/proposal-class-properties'],
};

2.导出ES module和Common js产物供使用者使用

完全可以使用babel或者tsc命令行工具进行代码编译处理,前面已经通过ts.config.json生成类型声明文件以及生成lib和esm文件夹,后序打包成的esm和cjs两种形式的代码分别输出到esm和lib文件夹,此处选择用gulp串起这个流程

先安装gulp相关依赖对代码进行合并优化和压缩

gulp会自动执行指定的任务,就像流水线,把资源放上去然后通过不同插件进行加工

为什么用gulp而不是webpack或者rollup?因为我们要做的是代码编译而非代码打包,(在搭建组件库的过程,即将ts代码转为js代码,将scss文件转为css文件,同时需要考虑到样式处理及其按需加载

  1. Gulp的配置更加简单直观,不需要像Webpack那样复杂的配置文件,更易于上手和使用。
  2. Gulp可以更加灵活地处理各种文件类型,不仅仅局限于JavaScript,也可以处理CSS、HTML、图片等文件,更适合于组件库的打包。
  3. Gulp的插件生态也很丰富,可以满足各种需求,比如压缩、合并、重命名等操作,而且插件之间可以组合使用,更加灵活。
  4. Gulp的执行速度更快,因为它是基于流式处理的,可以避免不必要的IO操作,提高打包速度。
  5. Gulp的社区活跃,有很多教程和案例可以参考,问题也更容易得到解决。

gulp官网:https://www.gulpjs.com.cn/docs/getting-started/creating-tasks/

1
npm install gulp gulp-babel --dev

新建gulpfile.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
const gulp = require("gulp");
const babel = require("gulp-babel");
const sass = require("gulp-sass")(require("sass"));
const autoprefixer = require("gulp-autoprefixer");
const cssnano = require("gulp-cssnano");
const through2 = require("through2");
// 编译脚本文件
// @param {string} babelEnv babel环境变量
// @param {string} destDir 目标目录

const paths = {
dest: {
lib: "lib", //commonjs文件存放的目录名,关注
esm: "esm", //ES module文件存放的目录名,暂时不关心
dist: "dist", //umd文件存放的目录名,暂时不关心
},
styles: "src/**/*.scss", //样式文件路径
scripts: [
"src/**/*.{ts,tsx}",
"!src/**/demo/*.{ts,tsx}",
"!src/**/__tests__/*.{ts,tsx}",
], //脚本路径,glob详细https://www.gulpjs.com.cn/docs/getting-started/explaining-globs/
};
//当前组件样式import './index.less'=>import './index.css'
//依赖的其他组件样式 import "../test-comp/style"=>import "../test-comp/style/css.js"
//依赖的其他组件样式 import '../test-comp/style/index.js'=>import '../test-comp/style/css.js'

function cssInction(content) {
return content
.replace(/\/style\/?'/g, "/style/css'")
.replace(/\/style\/?"/g, '/style/css"')
.replace(/\.scss/g, ".css");
}
function compileScripts(babelEnv, destDir) {
const { scripts } = paths;
process.env.BABEL_ENV = babelEnv;
return gulp
.src(scripts)
.pipe(babel()) //使用gulp-babel处理
.pipe(
through2.obj(function z(chunk, encoding, next) {
this.push(chunk.clone());
//找到style/index.js
if (chunk.path.match(/(\/|\\)style(\/|\\)index\.js/)) {
const content = chunk.contents.toString(encoding);
chunk.contents = Buffer.from(cssInction(content)); //文件内容处理
chunk.path = chunk.path.replace(/index\.js/, "css.js"); //文件重命名
this.push(chunk); //新增该文件
next();
} else {
next();
}
})
)
.pipe(gulp.dest(destDir)); //输出
}
function compileCJS() {
const { dest } = paths;
return compileScripts("cjs", dest.lib);
}
//编译esm
function compileESM() {
const { dest } = paths;
return compileScripts("esm", dest.esm);
}
//拷贝scss文件
function copyScss() {
return gulp
.src(paths.styles)
.pipe(gulp.dest(paths.dest.lib))
.pipe(gulp.dest(paths.dest.esm));
}
//生成css文件
function Scss2css() {
return gulp
.src(paths.styles)
.pipe(sass()) //处理scss文件
.pipe(autoprefixer()) //根据browserslistrc增加前缀
.pipe(cssnano({ zindex: false, reduceIdents: false })) //压缩
.pipe(gulp.dest(paths.dest.lib))
.pipe(gulp.dest(paths.dest.esm));
}
//串行执行编译脚本任务,避免环境变量影响
const buildScripts = gulp.series(compileCJS, compileESM);
//整体并行任务,后续加入样式处理 可以并行处理
const build = gulp.parallel(buildScripts, copyScss, Scss2css);
exports.build = build;
exports.default = build;
const gulp = require("gulp");
const babel = require("gulp-babel");
const less = require("gulp-less");
const autoprefixer = require("gulp-autoprefixer");
const cssnano = require("gulp-cssnano");
const through2 = require("through2");
// 编译脚本文件
// @param {string} babelEnv babel环境变量
// @param {string} destDir 目标目录

const paths = {
dest: {
lib: "lib", //commonjs文件存放的目录名,关注
esm: "esm", //ES module文件存放的目录名,暂时不关心
dist: "dist", //umd文件存放的目录名,暂时不关心
},
styles: "src/**/*.less", //样式文件路径
scripts: ["src/**/*.{ts,tsx}", "!src/**/demo/*.{ts,tsx}","!src/**/__tests__/*.{ts,tsx}"], //脚本路径,glob详细https://www.gulpjs.com.cn/docs/getting-started/explaining-globs/
};
//当前组件样式import './index.less'=>import './index.css'
//依赖的其他组件样式 import "../test-comp/style"=>import "../test-comp/style/css.js"
//依赖的其他组件样式 import '../test-comp/style/index.js'=>import '../test-comp/style/css.js'

function cssInction(content) {
return content
.replace(/\/style\/?'/g, "/style/css'")
.replace(/\/style\/?"/g, '/style/css"')
.replace(/\.less/g, ".css");
}
function compileScripts(babelEnv, destDir) {
const { scripts } = paths;
process.env.BABEL_ENV = babelEnv;
return gulp
.src(scripts)
.pipe(babel()) //使用gulp-babel处理
.pipe(
through2.obj(function z(chunk, encoding, next) {
this.push(chunk.clone());
//找到style/index.js
if (chunk.path.match(/(\/|\\)style(\/|\\)index\.js/)) {
const content = chunk.contents.toString(encoding);
chunk.contents = Buffer.from(cssInction(content)); //文件内容处理
chunk.path = chunk.path.replace(/index\.js/, "css.js"); //文件重命名
this.push(chunk); //新增该文件
next();
} else {
next();
}
})
)
.pipe(gulp.dest(destDir)); //输出
}
function compileCJS() {
const { dest } = paths;
return compileScripts("cjs", dest.lib);
}
//编译esm
function compileESM() {
const { dest } = paths;
return compileScripts("esm", dest.esm);
}
//拷贝less文件
function copyLess() {
return gulp
.src(paths.styles)
.pipe(gulp.dest(paths.dest.lib))
.pipe(gulp.dest(paths.dest.esm));
}
//生成css文件
function less2css() {
return gulp
.src(paths.styles)
.pipe(less()) //处理less文件
.pipe(autoprefixer()) //根据browserslistrc增加前缀
.pipe(cssnano({ zindex: false, reduceIdents: false })) //压缩
.pipe(gulp.dest(paths.dest.lib))
.pipe(gulp.dest(paths.dest.esm));
}
//串行执行编译脚本任务,避免环境变量影响
const buildScripts = gulp.series(compileCJS, compileESM);
//整体并行任务,后续加入样式处理 可以并行处理
const build = gulp.parallel(buildScripts, copyLess, less2css);
exports.build = build;
exports.default = build;

使用环境变量区分esm和cjs:

.babelrc.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module.exports = {
presets: ['@babel/env', '@babel/typescript', '@babel/react'],
plugins: ['@babel/plugin-transform-runtime', '@babel/proposal-class-properties'],
env: {
esm: {
presets: [
[
'@babel/env',
{
modules: false,
},
],
],
plugins: [
[
'@babel/plugin-transform-runtime',
{
useESModules: true,
},
],
],
},
},
};

配置package.json:

1
2
3
4
5
6
7
8
9
10
11
{
- "main": "index.js",
+ "main": "lib/index.js",
+ "module": "esm/index.js"
"scripts": {
...
+ "clean": "rimraf lib esm dist",
+ "build": "npm run clean && npm run build:types && gulp",
...
},
}

npm run build用来输出esm和lib两种方式的文件再使用gulp进行压缩优化

配置目标环境:

为了避免转译浏览器原生语法,新建.browserslistrc文件,写入适配要求,写入支持浏览器范围,作用域@babel/preset-env,

.browserslistrc:

1
2
3
>0.2%
not dead
not op_mini all

处理样式文件:

这里有个问题就是如果使用者没有使用scss预处理器,使用的是less或者css原生方案,那现有方案就搞不定,有以下4种预选方案:

1 告知业务方增加sass-loader,会导致业务方使用成本增加

2 打包出一份完整的css文件,全量引入,无法按需引入

3 css in js

4 提供一份style/css.js文件,引入组件css样式依赖,而不是scss依赖,组件库底层抹平差异

第3个方法优点是会让每个组件与自己的样式绑定,不需要开发者去维护样式依赖,但是缺点是:

(1)样式无法单独缓存

(2)style-components自身体积较大

(3)复写组件样式需要使用属性选择器或者使用styled-components自带方法

https://styled-components.com/docs/basics#installation

讲下第4点,这也是antd使用的方案,在搭建组件库过程中需要在alert/style/index.js中引入scss文件,是为了管理样式依赖,因为组件没有引入样式文件,需要使用者手动引入,假如:使用者引入Button,Button依赖了Icon,则需要手动引入调用组件的样式(Button)及其依赖的组件样式(Icon),遇到复杂的组件更麻烦,所以组件开发者提供了一份这样的js文件,使用者手动引入这个js文件,就能引入对应组件及其依赖组件的样式。

为什么组件不直接 import ‘./index.scss’,这样业务方可能不使用scss,就需要配置scss-loader,因此单独提供一份style/css.js文件,引入的是组件css样式文件依赖,组件库底层抹平差异

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

/**
* 生成css文件
*/
function Scss2css() {
return gulp
.src(paths.styles)
.pipe(scss()) // 处理scss文件
.pipe(autoprefixer()) // 根据browserslistrc增加前缀
.pipe(cssnano({ zindex: false, reduceIdents: false })) // 压缩
.pipe(gulp.dest(paths.dest.lib))
.pipe(gulp.dest(paths.dest.esm));
}

const build = gulp.parallel(buildScripts, copyScss, Scss2css);

// ...

执行npm run build,组件style目录下已经存在css文件,接下来我们需要一个alert/style/css.js来帮用户引入css文件:可以在gulpfile.js文件中处理scripts任务中截住style/index.js,生成style/css.js,并通过正则将引入的scss文件后缀改为css

1
npm i through2 --dev

gulpfile.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// ...
function cssInjection(content) {
return content
.replace(/\/style\/?'/g, "/style/css'")
.replace(/\/style\/?"/g, '/style/css"')
.replace(/\.scss/g, '.css');
}
/**
* 编译脚本文件
* @param {*} babelEnv babel环境变量
* @param {*} destDir 目标目录
*/
function compileScripts(babelEnv, destDir) {
const { scripts } = paths;
process.env.BABEL_ENV = babelEnv;
return gulp
.src(scripts)
.pipe(babel()) // 使用gulp-babel处理
.pipe(
through2.obj(function z(file, encoding, next) {
this.push(file.clone());
// 找到目标
if (file.path.match(/(\/|\\)style(\/|\\)index\.js/)) {
const content = file.contents.toString(encoding);
file.contents = Buffer.from(cssInjection(content)); // 文件内容处理
file.path = file.path.replace(/index\.js/, 'css.js'); // 文件重命名
this.push(file); // 新增该文件
next();
} else {
next();
}
}),
)
.pipe(gulp.dest(destDir));
}

// ...

再次打包运行npm run build,组件style目录下生成css.js文件,引入的index.css是由上一步scss转换来的css文件

按需加载:

在package.json中增加sideEffects属性,配合ES module达到tree shaking,(将样式依赖文件标注为side effects,避免被误删除)

1
2
3
4
5
6
7
8
//...
"sideEffects": [
"dist/*",
"esm/**/style/*",
"lib/**/style/*",
"*.scss"
]
//...

使用以下方式引入,可以做到js部分按需加载:

1
2
import { Alert } from 'july-design';
import 'july-design/esm/alert/style';

渲染器的核心功能就是挂载与更新

首先,有一个虚拟dom的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
const vnode = {
type:'div',
//使用props描述一个元素得特性
props:{
id:'foo'
},
children:[
{
type:'p',
children:'hello'
}
]
}

检查vnode.props字段可以使用el[key]=vnode.props[key]或者el.setAttribute(key,vnode.props[key]),先来了解下HTML Attributes和DOM Properties的不同

HTML Attributes和DOM Properties

1
<div id="foo"></div>

这个DOM对象有很多属性,HTML Attributes在DOM对象上有与之同名的DOM Properties,例如id=”my-input”对应el.id,但DOM Properties并不与HTML Attributes的名字总是一样,例如

1
2
3
<div class="foo">

</div>

class=’foo’对应的DOM Properties是el.className,不是所有的HTML Adttributes都有和它对应的DOM Properties,例如aria-*类的HTML Attributes就没有和它对应的DOM Properties

1
<input value='foo'/>

当用户修改了文本框的值,那么el.value的值时当前文本的值,而el.getAttribute(‘value’)仍然是之前的值

总之:HTML Attributes的作用是设置与之对应的DOM Properties的初始值

正确设置元素的属性

例子:

1
2
3
<button :disabled='false'>
Button
</button>

这个HTML模板会被编译成vnode:

1
2
3
4
5
6
const button = {
type: 'button',
props: {
disabled:false
}
}

这里的props.disabled的值时空字符串,如果在渲染器中调用setAttribute函数设置属性,相当于:

1
el.setAttribute('disabled',false)

用户本意是不禁用,但是用setAttribute按钮仍然被禁用了,这是因为使用setAttribute函数设置的值总是会被字符串化,等价于

1
el.setAttribute('disabled','false')

el.disabled的值时布尔值,我们不关心值是什么,只要disabled属性存在,按钮就被禁用,不使用setAttribute,用el.disabled=false

但是如果是下面的模板:

1
2
3
<button disabled>
Button
</button>

对应的vnode:

1
2
3
4
5
6
const button = {
type:'button',
props: {
disabled: ''
}
}

用DOM Properties设置元素属性时,el.disabled=’’,类型转换会设置为el.disabled=false,用户本意是禁用按钮,则只能优先设置setAttribute,如果是空字符串则手动矫正

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function mountElement(vnode,container) {
const el = createElement(vnode.type)
//省略children处理
if(vnode.props){
for(const key in vnode.props){
if(key in vnode.props){
//用in操作判断key是否存在对应的DOM Properties
if(key in el){
//获取该DOM Properties的类型
const type = el[key]
const value = vnode.props[key]
if(type === 'boolean' && value ===''){
e[key]=true
}else{
e[key]=value
}
}else{
el.setAttribute(key,vnode.props[key])
}

}
}
}
insert(el,container)
}

前置知识

屏幕尺寸:以屏幕对角线的长度计算,单位是英寸

像素 pixel:显示屏画面上表示出来的最小单位

屏幕分辨率:一个屏幕具体由多少个像素点组成,单位是px

物理像素:在同一个设备上,他的物理像素是固定的,也就是厂家在生产显示设备时就决定的实际点的个数,对于不同设备物理像素点的大小是不一样的

逻辑像素(设备独立像素):(与设备无关的逻辑像素,代表可以通过程序控制使用的虚拟像素)

设备像素比dpr:计算公式为:DPR = 物理像素/逻辑像素

当设备像素比为1:1时,使用1(1×1)个设备像素显示1个CSS像素;

当设备像素比为2:1时,使用4(2×2)个设备像素显示1个CSS像素;

当设备像素比为3:1时,使用9(3×3)个设备像素显示1个CSS像素。

视口viewport:

viewport指的是视口,它是浏览器或者app中webview显示页面的区域,一般,PC端的视口指的是浏览器窗口区域,而移动端有三个视口:

layout viewport:布局视口

visual viewport:视觉视口

ideal viewport:理想视口

布局视口(layout viewport):

由浏览器提出的一种虚拟的布局视口,用来解决页面在收上显示的问题,这种视口可以通过标签设置viewport来改变,移动设备上的浏览器会把自己默认的viewport设为980px或者1024px,也可能是其它值,这个是由设备自己决定的),但带来的后果就是浏览器会出现横向滚动条,因为浏览器可视区域的宽度是比这个默认的viewport的宽度要小的。

我们可以通过document.documentElement.clientWidth来获取布局视口大小

视觉视口(visual viewport)

它指的是浏览器的可视区域,也就是我们在移动端设备上能够看到的区域。默认与当前浏览器窗口大小相等,当用户对浏览器进行缩放时,不会改变布局视口的大小,但会改变视觉窗口的大小。

meta viewport

对于移动端页面,可以采用<meta>标签来配置视口大小和缩放等。

1
2
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
复制代码
  • width:该属性被用来控制视窗的宽度,可以将width设置为320这样确切的像素数,也可以设为device-width这样的关键字,表示设备的实际宽度,一般为了自适应布局,普遍的做法是将width设置为device-width
  • height:该属性被用来控制视窗的高度,可以将height设置为640这样确切的像素数,也可以设为device-height这样的关键字,表示设备的实际高度,一般不会设置视窗的高度,这样内容超出的话采用滚动方式浏览。
  • initial-scale:该属性用于指定页面的初始缩放比例,可以配置0.0~10的数字,initial-scale=1表示不进行缩放,视窗刚好等于理想视窗,当大于1时表示将视窗进行放大,小于1时表示缩小。这里只表示初始视窗缩放值,用户也可以自己进行缩放,例如双指拖动手势缩放或者双击手势放大。安卓设备上的initial-scale默认值: 无默认值,一定要设置,这个属性才会起作用。在iphone和ipad上,无论你给viewport设的宽的是多少,如果没有指定默认的缩放值,则iphone和ipad会自动计算这个缩放值,以达到当前页面不会出现横向滚动条(或者说viewport的宽度就是屏幕的宽度)的目的。
  • maximum-scale:该属性表示用户能够手动放大的最大比例,可以配置0.0~10的数字。
  • minimum-scale:该属性类似maximum-scale,用来指定页面缩小的最小比例。通常情况下,不会定义该属性的值,页面太小将难以浏览。
  • user-scalable:该属性表示是否允许用户手动进行缩放,可配置no或者yes。当配置成no时,用户将不能通过手势操作的方式对页面进行缩放。

这里需要注意的是viewport只对移动端浏览器有效,对PC端浏览器是无效的。

rem适配:

是CSS3新增的一个相对单位,是指相对于根元素的字体大小的单位。

  • 将屏幕宽度分为10份,设置html的font-size为window.innerWidth/10px
  • 1rem=window.innerWidth/10px
  • 根据UI的px计算相应rem:比如盒子宽320px,(320px/window.innerWidth)*10=5.333rem
  • 将rem转为不同的px尺寸在不同手机上呈现:5.333rem*window.innerWidth/10px
1
2
3
4
5
//给html标签添加font-size
document.addEventListener('DOMContentLoaded', function(e) {
document.getElementsByTagName('html')[0].style.fontSize = window.innerWidth / 10 + 'px';
}, false);

使用Sass定义一个ps2rem函数

1
2
3
4
5
6
7
@funtion px2rem($px){
$rem: 75px;//750/10
@return ($px/$rem)+rem
}
.box1 {
width: px2rem(320px);//(320/750)*10=4.266rem
}

vw,vh适配:

vw(Viewport Width)vh(Viewport Height)是基于视图窗口的单位,是css3中提出来的,基于视图窗口的单位。

vh、vw方案即将视觉视口宽度 window.innerWidth和视觉视口高度 window.innerHeight 等分为 100 份。

上面的flexible方案就是模仿这种方案,因为早些时候vw还没有得到很好的兼容。

  • vw(Viewport's width)1vw等于视觉视口的1%
  • vh(Viewport's height) :1vh 为视觉视口高度的1%
  • vmin : vwvh 中的较小值
  • vmax : 选取 vwvh 中的较大值

如果按视觉视口为375px,那么1vw = 3.75px,这时UI给定一个元素的宽为75px(设备独立像素),我们只需要将它设置为75 / 3.75 = 20vw

1
2
@device-width: 375
@vw: (100vw/device-width)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<template>
<div class="songyao">
<h1>{{ username }}</h1>
<p>
了解脚手架及脚手架指令请移步个人博客<br>
check out the
<a href="http://47.100.126.169/zmengBlog" target="_blank" rel="noopener">逐梦博客</a>.
</p>
<p>微信公众号:<span class="wx_name">前端南玖</span></p>
</div>
</template>

<script>
export default {
name: 'songyao',
data() {
return {
username: 'songyao-cli(vue 模板)'
}
},
}
</script>

<style lang="less">
.songyao{
h1{

font-size: 24*@vw;
}
p{

font-size: 16*@vw;
}
.wx_name{
color:brown;
}
}

</style>

viewport和px

这种方案可以让我们在开发时不用关注设备屏幕尺寸的差异,直接按照设计稿上的标注进行开发,也无需单位的换算,直接用px。HTML 的 head 标签里加入 <meta name="viewport" content="width={设计稿宽度}, initial-scale={屏幕逻辑像素宽度/设计稿宽度}" >

假如UI给我们提供的设计稿宽度时375px,我们则需要将页面的viewport的width设为375,然后再根据设备的逻辑像素将页面进行整体放缩。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export function initViewport() {
const width = 375; // 设计稿宽度
const scale = window.innerWidth / width
// console.log('scale', scale)
let meta = document.querySelector('meta[name=viewport]')
let content = `width=${width}, init-scale=${scale}, user-scalable=no`
if(!meta) {
meta = document.createElement('meta')
meta.setAttribute('name', 'viewport')
document.head.appendChild(meta)
}
meta.setAttribute('content', content)
}


总结:

rem:

  • 适配原理复杂
  • 需要使用js
  • 设计稿标注px换算到css的rem计算简单
  • 方案灵活,即能实现整体缩放,又能实现局部不缩放

vw:

  • 适配原理简单
  • 不需要使用js
  • 设计稿标注的px换算为css的vw计算复杂

viewport+px:

  • 适配原理简单
  • 需要使用js
  • 直接使用设计稿标注无需换算
  • 方案死板,只能实现页面级别肢体缩放

参考:https://juejin.cn/post/7085931616136069156#heading-19

https://juejin.cn/post/6844903590968950797#heading-7

职责链模式:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间得到耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止

灵活可拆分的职责链结点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
let order500 = function(orderType,pay,stock){
if(orderType === 1 && pay===true){
console.log('500元定金,100元优惠')
}else{
return 'nextSuccessor'
}
}
let order200 = function(orderType,pay,stock){
if(orderType === 2 && pay === true){
console.log('200元定金,50元优惠')
}else{
return 'nextSuccessor'
}
}
let orderNormal = function(orderType,pay,stock){
if(stock>0){
console.log('普通购买,无优惠')
}else{
console.log('库存不足')
}
}
let Chain = function(fn){
this.fn = fn;
this.successor = null
}
Chain.prototype.setNextSuccessor = function(successor){
return this.successor = successor
}
Chain.prototype.passResquest = function(){
let ret = this.fn.apply(this,arguments)
//console.log(ret)
if(ret === 'nextSuccessor'){
return this.successor && this.successor.passResquest.apply(this.successor,arguments)
}
return ret
}
let chainOrder500 = new Chain(order500)//500元定金,100元优惠
let chainOrder200 = new Chain(order200)
let chainOrderNormal = new Chain(orderNormal)
chainOrder500.setNextSuccessor(chainOrder200)
chainOrder200.setNextSuccessor(chainOrderNormal)
chainOrder500.passResquest(1,true,500)//500元定金,100元优惠
chainOrder500.passResquest(2,true,500)//走nextSuccessor,200元定金,50元优惠
chainOrder500.passResquest(3,true,500)//继续nextSuccessor,普通购买,无优惠
chainOrder500.passResquest(1,false,0)//一直nextSuccessor到库存不足

当要加入一个结点:

1
2
3
4
5
6
let order300 = function(){
...
}
chainOrder300 = new Chain(order300)
chainOrder500.setNextSuccessor(chainOrder300)
chainOrder300.setNextSuccessor(chainOrder200)

AOP模式实现职责链:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
let order500 = function(orderType,pay,stock){
if(orderType === 1 && pay===true){
console.log('500元定金,100元优惠')
}else{
return 'nextSuccessor'
}
}
let order200 = function(orderType,pay,stock){
if(orderType === 2 && pay === true){
console.log('200元定金,50元优惠')
}else{
return 'nextSuccessor'
}
}
let orderNormal = function(orderType,pay,stock){
if(stock>0){
console.log('普通购买,无优惠')
}else{
console.log('库存不足')
}
}
Function.prototype.after = function(fn){
let self = this
return function(){
let ret = self.apply(this,arguments)
if(ret === 'nextSuccessor'){
return fn.apply(this,arguments)
}
return ret;
}
}
let order = order500.after(order200).after(orderNormal)
order(1,true,500)
order(2,true,500)
order(1,false,500)

命令模式的应用场景:

有时候向某些对象发送请求,但是不知道请求接收者和发送者是谁,也不知请求操作是什么,此时用一种松耦合的方式来设计程序,使得发送者和接收者能够消除耦合关系

面向对象形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<body>
<button id="button1">点击按钮1</button>
<script>
let btn1 = document.getElementById('button1')
//安装命令的函数
let setCommand = function(button,command){
button.onclick = function(){
command.execute()
}
}
//命令接收者
let MenuBar = {
refresh :function(){
console.log("刷新菜单目录")
}
}
//command对象
let RefreshMenuBarCommand = function(receiver){
this.receiver=receiver
}
RefreshMenuBarCommand.prototype.execute = function(){
this.receiver.refresh()
}
//命令接收者接传入到command对象,把command对象安装到button上
let refreshMenuBarCommand = new RefreshMenuBarCommand(MenuBar)
setCommand(button1,refreshMenuBarCommand)
</script>
</body>

闭包形式

可以使用闭包的命令模式,将命令接收者封闭在闭包产生的环境中,执行命令的操作更简单,仅仅是执行回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let btn1 = document.getElementById('button1')
//安装命令的函数
let setCommand = function(button,command){
button.onclick = function(){
command.execute()
}
}
//命令接收者
let MenuBar = {
refresh :function(){
console.log("刷新菜单目录")
}
}
//command对象
let RefreshMenuBarCommand = function(receiver){
return function(){
return receiver.refresh()
}
}
//命令接收者传入到command对象,把command对象安装到button上
let refreshMenuBarCommand = new RefreshMenuBarCommand(MenuBar)
setCommand(button1,refreshMenuBarCommand)

使用命令模式可以方便给对象增加撤销命令操作,撤销命令是执行命令的反向操作,文本编辑器的Ctrl+Z和围棋中的悔棋都是撤销命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<body>
<div id="ball" style="position: absolute;background: #000;width:50px;height:50px"></div>
<input type="text" id="pos">
<button id="moveBtn">开始移动</button>
<button id="cancleBtn">撤销命令</button>
<script>
let ball = document.getElementById('ball')
let pos = document.getElementById('pos')
let moveBtn = document.getElementById('moveBtn')
let cancleBtn = document.getElementById('cancleBtn')
let moveCommand;
let MoveCommand = function (receiver,pos) {
this.receiver = receiver;
this.pos = pos;
this.oldPos=null
}
MoveCommand.prototype.excute = function(){
this.receiver.start('left',this.pos,1000,'strongEaseout')
oldPos = this.receiver.dom.getBoundingClientRec()[this.receiver.propertyName]
}
//执行反向操作
MoveCommand.prototype.undo = function(){
this.receiver.start('left',this.oldPos,1000,'strongEaseout')
}
moveBtn.onclick=function(){
let animate = new Animate(ball)
moveCommand = new MoveCommand(animate,pos.value)
moveCommand.excute()
}
cancleBtn.onclick=function(){
moveCommand.undo()
}
</script>
</body>

宏命令

是一组命令的集合,通过执行宏命令可以执行一批命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
let quitCommand = {
execute: function(){
console.log('退出')
}
}
let loginCommand ={
execute: function(){
console.log('登录')
}
}
let MacroCommand = function(){
return {
commandList:[],
add: function(command){
this.commandList.push(command)
},
execute:function(){
for(let i=0;command;command = this.commandList[i]){
command.execute()
}

}

}
}
let macroCommand = new MacroCommand()
macroCommand.add(quitCommand)
macroCommand.add(loginCommand)
macroCommand.execute()

总结

一般,命令模式都会在command命令对象中保存一个接收者负责真正执行客户的请求,这种命令模式是傻瓜式命令,它只负责把客户的命令转发给接收者执行,让请求发起者和接收者之间尽可能解耦

聪明式命令对象可以直接实现请求,不需要接收者的存在,形式上和策略模式很像,通过使用意图分辨它们,策略模式指向的问题域更小,所有策略目标一致,它们只是达到这个目标的不同手段,命令模式指向的问题域更广,command对象解决的目标更具发散性。

eg:

类组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user);
};

handleClick = () => {
setTimeout(this.showMessage, 3000);
};

render() {
return <button onClick={this.handleClick}>Follow</button>;
}
}

函数式组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};

const handleClick = () => {
setTimeout(showMessage, 3000);
};

return (
<button onClick={handleClick}>Follow</button>
);
}

在React中props是不可变的,所以它们永远不会改变,然而,类组件中,this是且永远是可变的

类组件中this存在的意义:React本身随着时间推移而改变,以便你可以在渲染方法以及生命周期方法中得到最新的实例.

如果希望类组件中能在一次特定渲染中捕获那一次渲染所用的props或者state,可以使用闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ProfilePage extends React.Component {
render() {
// Capture the props!
const props = this.props;

// Note: we are *inside render*.
// These aren't class methods.
const showMessage = () => {
alert('Followed ' + props.user);
};

const handleClick = () => {
setTimeout(showMessage, 3000);
};

return <button onClick={handleClick}>Follow</button>;
}
}

函数式组件想捕获最新的props和state?

用useRef

1
2
3
4
5
6
7
8
9
10
11
12
function MessageThread() {
const [message, setMessage] = useState('');

// 保持追踪最新的值。
const latestMessage = useRef('');
useEffect(() => {
latestMessage.current = message;
});

const showMessage = () => {
alert('You said: ' + latestMessage.current);
};

总结于: