setup只会在被挂载时执行一次,返回值由两种情况:
1 返回一个函数,该函数将作为组件的render函数
2 返回一个对象,该对象包含的数据将暴露给模板使用
setup函数第一个参数为外部为组件传递的props数据对象,第二个参数为setupContext对象,其中保存着组件接口相关的数据和方法:slots,emit,attrs,expose
1 | function mountComponent(vnode,container,anchor) { |
setup只会在被挂载时执行一次,返回值由两种情况:
1 返回一个函数,该函数将作为组件的render函数
2 返回一个对象,该对象包含的数据将暴露给模板使用
setup函数第一个参数为外部为组件传递的props数据对象,第二个参数为setupContext对象,其中保存着组件接口相关的数据和方法:slots,emit,attrs,expose
1 | function mountComponent(vnode,container,anchor) { |
MyComponent组件模板:
1 | <template> |
当在父组件中使用
1 | <MyComponent> |
父组件模板会被编译成如下渲染函数:
1 | function render(){ |
组件模板中的插槽内容会被编译为插槽函数,而插槽函数的返回值就是具体的插槽内容
MyComponent的模板会被编译成如下渲染函数:
1 | function render() { |
渲染插槽内容的过程,就是调用插槽函数并渲染由其返回的内容的过程,在运行时实现上,插槽依赖于setupContext中的slots对象,
1 | function mountComponent(vnode,container,anchor) { |
1 | //双端diff算法 |
在实测中性能最优,它借鉴了文本Diff中的预处理思路,先处理新旧两组结点中相同的前置结点和相同的后置结点,当前前置结点和后置结点全部处理完毕后,如果无法简单地通过挂载新节点或者卸载已经不存在的结点来完成更新,则需要根据结点的索引关系,构造一个最长递增子序列,最长递增子序列所指向的结点即为不需要移动的结点,构造source数组,用来存储新的一组子节点在旧子节点中的位置索引,后面用它来计算最长递增子序列
1 | function patchKeyedChildren(n1,n2,container) { |
搭建属于自己的组件库,方便在项目中导入组件,避免重复造轮子。
组件编写:react,typescript
代码规范:eslint,prettier
打包编译:gulp,babel
文档编写:dumi
样式处理:scss
1 | .github |
1 | mkdir my-ui |
直接使用@umijs.fabric的配置
1 | npm i --dev @umi/fabric prettier |
.eslintrc.js:
1 | module.exports = { |
.prettierrc.js:
1 | const fabric = require('@umijs/fabric'); |
.stylelintrc.js:
1 | module.exports = { |
关于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 | //... |
后面使用npm run commit代替git commit生成规范的commit message,也可以手写,但是要符合规范
配置typeScript:
1 | npm i typescript --dev |
新建tsconfig.json:
1 | { |
编写组件代码:
1 | alert |
下载react相关依赖:
1 | npm i react react-dom @types/react @types/react-dom --dev # 开发时依赖,宿主环境一定存在 |
用dumi作为组件文档站点工具,并兼具开发调试功能
1 | npm i dumi serve --dev |
增加npm scripts到package.json
1 | "scripts": { |
配置.umirc.ts配置文件:
1 | import { defineConfig } from 'dumi'; |
首页配置:
docs/index.md:
1 | --- |
可以参考dumi文档进行配置
index.md:
1 | -- __tests__ |
将文档部署到github pages
下载cross–env区分环境变量
1 | npm i cross-env --dev |
package.json:
1 | "scripts": { |
.umirc.ts
1 | import { defineConfig } from 'dumi'; |
下载gh-pages完成一键部署:
1 | npm i gh-pages --dev |
package.json:
1 | "scripts":{ |
执行npm run deploy:site后就能在${username}.github.io/${repo}看到自己的组件库站点
1 | name: github pages |
先下载cpr,这样运行下面的npm run build:typs时会将lib的声明文件拷贝一份,并将文件夹重命名为esm,后面存放ES module形式的组件,这样做的原因是保证用户手动按需引入组件的时候可以获取自动提示
**使用typescript编写的组件库,应该利用类型系统的好处,我们可以生成类型声明文件,**并在package.json中定义入口:
1 | { |
配置tsconfig.build.json:
1 | { |
执行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 | >0.2% |
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 | module.exports = { |
完全可以使用babel或者tsc命令行工具进行代码编译处理,前面已经通过ts.config.json生成类型声明文件以及生成lib和esm文件夹,后序打包成的esm和cjs两种形式的代码分别输出到esm和lib文件夹,此处选择用gulp串起这个流程
先安装gulp相关依赖对代码进行合并优化和压缩
gulp会自动执行指定的任务,就像流水线,把资源放上去然后通过不同插件进行加工
为什么用gulp而不是webpack或者rollup?因为我们要做的是代码编译而非代码打包,(在搭建组件库的过程,即将ts代码转为js代码,将scss文件转为css文件,同时需要考虑到样式处理及其按需加载
gulp官网:https://www.gulpjs.com.cn/docs/getting-started/creating-tasks/
1 | npm install gulp gulp-babel --dev |
新建gulpfile.js:
1 | const gulp = require("gulp"); |
使用环境变量区分esm和cjs:
.babelrc.js
1 | module.exports = { |
配置package.json:
1 | { |
npm run build用来输出esm和lib两种方式的文件再使用gulp进行压缩优化
配置目标环境:
为了避免转译浏览器原生语法,新建.browserslistrc文件,写入适配要求,写入支持浏览器范围,作用域@babel/preset-env,
.browserslistrc:
1 | >0.2% |
这里有个问题就是如果使用者没有使用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 | // ... |
执行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 | // ... |
再次打包运行npm run build,组件style目录下生成css.js文件,引入的index.css是由上一步scss转换来的css文件
在package.json中增加sideEffects属性,配合ES module达到tree shaking,(将样式依赖文件标注为side effects,避免被误删除)
1 | //... |
使用以下方式引入,可以做到js部分按需加载:
1 | import { Alert } from 'july-design'; |
渲染器的核心功能就是挂载与更新
首先,有一个虚拟dom的代码如下:
1 | const vnode = { |
检查vnode.props字段可以使用el[key]=vnode.props[key]或者el.setAttribute(key,vnode.props[key]),先来了解下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 | <div class="foo"> |
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 | <button :disabled='false'> |
这个HTML模板会被编译成vnode:
1 | const button = { |
这里的props.disabled的值时空字符串,如果在渲染器中调用setAttribute函数设置属性,相当于:
1 | el.setAttribute('disabled',false) |
用户本意是不禁用,但是用setAttribute按钮仍然被禁用了,这是因为使用setAttribute函数设置的值总是会被字符串化,等价于
1 | el.setAttribute('disabled','false') |
el.disabled的值时布尔值,我们不关心值是什么,只要disabled属性存在,按钮就被禁用,不使用setAttribute,用el.disabled=false
但是如果是下面的模板:
1 | <button disabled> |
对应的vnode:
1 | const button = { |
用DOM Properties设置元素属性时,el.disabled=’’,类型转换会设置为el.disabled=false,用户本意是禁用按钮,则只能优先设置setAttribute,如果是空字符串则手动矫正
1 | function mountElement(vnode,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 | <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> |
device-width
。device-height
这样的关键字,表示设备的实际高度,一般不会设置视窗的高度,这样内容超出的话采用滚动方式浏览。0.0~10
的数字,initial-scale=1表示不进行缩放,视窗刚好等于理想视窗,当大于1时表示将视窗进行放大,小于1时表示缩小。这里只表示初始视窗缩放值,用户也可以自己进行缩放,例如双指拖动手势缩放或者双击手势放大。安卓设备上的initial-scale默认值: 无默认值,一定要设置,这个属性才会起作用。在iphone和ipad上,无论你给viewport设的宽的是多少,如果没有指定默认的缩放值,则iphone和ipad会自动计算这个缩放值,以达到当前页面不会出现横向滚动条(或者说viewport的宽度就是屏幕的宽度)的目的。0.0~10
的数字。no或者yes
。当配置成no时,用户将不能通过手势操作的方式对页面进行缩放。这里需要注意的是viewport
只对移动端浏览器有效,对PC端浏览器是无效的。
是CSS3新增的一个相对单位,是指相对于根元素的字体大小的单位。
1 | //给html标签添加font-size |
使用Sass定义一个ps2rem函数
1 | @funtion px2rem($px){ |
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
: vw
和 vh
中的较小值vmax
: 选取 vw
和 vh
中的较大值如果按视觉视口为375px
,那么1vw = 3.75px
,这时UI
给定一个元素的宽为75px
(设备独立像素),我们只需要将它设置为75 / 3.75 = 20vw
1 | @device-width: 375 |
1 | <template> |
这种方案可以让我们在开发时不用关注设备屏幕尺寸的差异,直接按照设计稿上的标注进行开发,也无需单位的换算,直接用px。HTML 的 head 标签里加入 <meta name="viewport" content="width={设计稿宽度}, initial-scale={屏幕逻辑像素宽度/设计稿宽度}" >
。
假如UI给我们提供的设计稿宽度时375px,我们则需要将页面的viewport的width设为375,然后再根据设备的逻辑像素将页面进行整体放缩。
1 | export function initViewport() { |
rem:
vw:
viewport+px:
职责链模式:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间得到耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止
1 | let order500 = function(orderType,pay,stock){ |
当要加入一个结点:
1 | let order300 = function(){ |
1 | let order500 = function(orderType,pay,stock){ |
有时候向某些对象发送请求,但是不知道请求接收者和发送者是谁,也不知请求操作是什么,此时用一种松耦合的方式来设计程序,使得发送者和接收者能够消除耦合关系
1 | <body> |
可以使用闭包的命令模式,将命令接收者封闭在闭包产生的环境中,执行命令的操作更简单,仅仅是执行回调函数
1 | let btn1 = document.getElementById('button1') |
使用命令模式可以方便给对象增加撤销命令操作,撤销命令是执行命令的反向操作,文本编辑器的Ctrl+Z和围棋中的悔棋都是撤销命令
1 | <body> |
是一组命令的集合,通过执行宏命令可以执行一批命令
1 | let quitCommand = { |
一般,命令模式都会在command命令对象中保存一个接收者负责真正执行客户的请求,这种命令模式是傻瓜式命令,它只负责把客户的命令转发给接收者执行,让请求发起者和接收者之间尽可能解耦
聪明式命令对象可以直接实现请求,不需要接收者的存在,形式上和策略模式很像,通过使用意图分辨它们,策略模式指向的问题域更小,所有策略目标一致,它们只是达到这个目标的不同手段,命令模式指向的问题域更广,command对象解决的目标更具发散性。
1 | class ProfilePage extends React.Component { |
1 | function ProfilePage(props) { |
在React中props是不可变的,所以它们永远不会改变,然而,类组件中,this是且永远是可变的
类组件中this存在的意义:React本身随着时间推移而改变,以便你可以在渲染方法以及生命周期方法中得到最新的实例.
如果希望类组件中能在一次特定渲染中捕获那一次渲染所用的props或者state,可以使用闭包
1 | class ProfilePage extends React.Component { |
用useRef
1 | function MessageThread() { |
总结于: