Fork me on GitHub
Xiaojun's Blog

  • Home

  • Tags

  • Archives

object-fit各种拉伸算法

发表于 2021-02-05
| 字数: 2.1k

需求来源

object-fit 可以很方便的对 img 标签做拉伸适配,但它一是兼容性不太好,二是只能在可替换元素中使用,如果我们想对一个 div 做类似调整,它就无能为力了,所以这种场景下就需要我们对 object-fit 的各种算法有一定了解才能手动实现。

算法实现

object-fit 一共有 fill | contain | cover | none | scale-down 五种拉伸方式,以下是针对每种方式的算法实现:

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
// Cover
// 被替换的内容将被缩放,以在填充元素的内容框时保持其宽高比。 整个对象在填充盒子的同时保留其长宽比,因此如果宽高比与框的宽高比不匹配,该对象将被添加“黑边”。
function cover(containerSize, elementSize) {
const containerRatio = containerSize.width / containerSize.height
const elementRatio = elementSize.width / elementSize.height

let width, height

if (containerRatio > elementRatio) {
width = containerSize.width
height = containerSize.width / elementRatio
} else {
width = containerSize.height * elementRatio
height = containerSize.height
}

return { width, height }
}

// Contain
// 被替换的内容在保持其宽高比的同时填充元素的整个内容框。如果对象的宽高比与内容框不相匹配,该对象将被剪裁以适应内容框。
function contain(containerSize, elementSize) {
const containerRatio = containerSize.width / containerSize.height
const elementRatio = elementSize.width / elementSize.height

let width, height

if (containerRatio < elementRatio) {
width = containerSize.width
height = containerSize.width / elementRatio
} else {
width = containerSize.height * elementRatio
height = containerSize.height
}

return { width, height }
}

// Fill
// 被替换的内容正好填充元素的内容框。整个对象将完全填充此框。如果对象的宽高比与内容框不相匹配,那么该对象将被拉伸以适应内容框。
function fill(containerSize) {
return containerSize
}

// None
// 被替换的内容将保持其原有的尺寸。
function none(elementSize) {
return elementSize
}

// ScaleDown
// 内容的尺寸与 none 或 contain 中的一个相同,取决于它们两个之间谁得到的对象尺寸会更小一些。
function scaleDown(containerSize, elementSize) {
if (elementSize.width > containerSize.width || elementSize.height > containerSize.height) {
return contain(containerSize, elementSize)
} else {
return none(containerSize)
}
}

示例

具体示例可以参考 https://codesandbox.io/s/object-fit-gechonglashensuanfa-23uhd

如何实现一个可以传入自定义动画的angular组件

发表于 2020-06-07
| 字数: 1.5k

学习 angular 过程中想开发一个允许传入自定义动画的 Popup 组件,本以为是个简单的事情,没想到中间断断续续耗费了我一周时间。

Popup 组件需求

  • 允许设置弹出位置,暂时只需要考虑 上 下 左 右 中。
  • 组件默认有自己动画,根据位置不同,默认动画也不同,比如位置为 上,那么动画就是从上方滑入,为 中,那么动画就是从中间淡入。用户可以传入一个自定义动画配置以替换默认动画效果。
  • 支持动画结束后发射事件,比如我想依靠这个 Popup 组件实现一个动态 Toast 组件(通过 componentFactory),Toast 组件在一定时间后自动销毁,那么就需要 Popup 组件支持在动画结束后发送一个事件,然后 Toast Service 监听它,而后调用 destroy,实现一个动态组件的方法可以看这里。

根据以上需求,结果我翻遍 Google,看别人 ui 库源码都不能满足我要求,因为他们组件动画都是写死的,要么是通过 @Component 的元数据 animations 配置,要么是类名 + css 实现,根本没法让外部通过属性传入。
但功夫不负有心人,终于我在 StackOverflow 翻到一个答案,那就是使用 AnimationBuilder,简而言之就是它允许你手动为元素构建一个动画,官方 demo 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// import the service from BrowserAnimationsModule
import { AnimationBuilder } from '@angular/animations'
// require the service as a dependency
class MyCmp {
constructor(private _builder: AnimationBuilder) {}

makeAnimation(element: any) {
// first define a reusable animation
const myAnimation = this._builder.build([
style({ width: 0 }),
animate(1000, style({ width: '100px' })),
])

// use the returned factory object to create a player
const player = myAnimation.create(element)

player.play()
}
}

🎉 最终效果

https://stackblitz.com/edit/ngx-popup-demo

最终封装后的组件已发布到 npm,有需要的可以看看,没准开发时可以帮你节省不少时间呢。

结束语

作为前端真的很有必要封装一个 Popup 组件,Vue 也好,React 也好,Angular 也好。很多常见弹层效果都可以依赖它进行二次封装,Toast 组件可以用它,Modal 组件可以用它,Notify 组件可以用它,Drawer 组件可以用它,各种底部弹出的 Picker 组件也可以用它,性价比实在是太高了。。🐮🍺

纯css实现保持div宽高比

发表于 2020-02-28
| 字数: 485

保持 div 宽高比这个需求其实完全可以使用 css 完成,以 16:9 为例

1
2
3
<div class="container">
<div class="cover"></div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
.container {
width: 200px;
height: 200px;
border: 2px solid gray;
resize: both;
overflow: auto;
}

.cover {
padding-bottom: 56.25%;
background: red url(https://i.loli.net/2020/02/28/MbkhTHzosq87XSG.jpg) no-repeat center / cover;
}

其中 padding-bottom: 56.25%; 是关键点,因为 css 中 padding 为百分比时是计算父元素宽度,所以 9 / 16 * 100 = 56.25

在线预览

记js中一些类型转换的坑

发表于 2019-12-27
| 字数: 1.7k

类型转换从初学 js 时就接触,但一直没能记牢,最近面试吃了大亏,自尊心深受打击,还是好好整理一下吧 🌚

显式转换

Number() 转换规则

Z9DMgikfOat5spS

Boolean() 转换规则

CmPU5EbuJyFLKXo

String() 以及不同对象 toString() 转换规则

FDGyP9dIbaKXTLC

隐式转换

== 操作符隐式转换

  1. 有且仅有一个操作数是 Boolean 会先将 Boolean 转化为 Number (true -> 1, false -> 0)
  2. 一个操作数是 String 另一个是 Number 会先将 String 转化为 Number
  3. 有且仅有一个操作数是对象类型则会调用 valueOf()、toString() 进行转化

举 🌰 子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
'true' == true // false
// 根据“规则 1”所知第一次将会转化为 `'true' == 1`,然后转化后又会符合“规则 2”,所以将重新转化为 `NaN == 1`

[1] == 1 // true
// 数组根据“规则 3”调用 valueOf() 依然为 `[1] == 1`,然后通过 toString() 转为 `'1' == 1`,最后根据“规则 2”转为 `1 == 1`

null == false // false
null == true // false
undefined == false // false
undefined == true // false
// 同为基本类型并且不符合 3 种规则中任意一种,所以都不进行转换,结果全为 false

[1, 2, 3] == [1, 2, 3] // false
// 这个没规则符合不会进行转换

+ 号运算符隐式转换

  1. 若 + 号两侧其中一个为字符串类型,那么会将另一侧也转为字符串类型,然后进行字符串拼接
  2. 若 + 号两侧其中一个为数字类型,那么会将另一侧也转为数字类型,然后进行计算
  3. 若 + 号右侧为对象类型,会先将它转化为字符串类型
  4. 若 + 号左侧声明了一个大括号那种对象,很多情况下 js 引擎会将它认成一个代码块而忽略计算
  5. 若 + 号左侧没内容,会先将右侧内容转为数字类型

举 🌰 子

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

'foo' + + 'bar' // 'fooNaN'
// 这个例子中第 2 个 + 号和 'bar' 是一起的,所以要按照“规则 2”来转换,
// 等于 'foo' + Number('bar') -> 'foo' + 'NaN'

[] + [] // ''
// 两侧通过 valueOf()、toString() 转化都得到 '',所以拼接后依然为一个空字符串

[] + {} // '[object Object]'
// 左侧通过转化得到 '',右侧通过转化得到 [object Object],所以拼接后就是最终结果

{} + [] // 0
// 这个莫名其妙的结果是因为 js 将 {} 解析成了一个代码块,只有 +[] 参与了运算

{} + {} // '[object Object][object Object]'
// 这个结果和“规则 4”有出入,测试环境为 Chrome 79.0.3945.88

- 号运算符

  • 若 - 号两侧不为数字类型则全转成数字类型后再进行计算
  • 若 - 号左侧没内容,会先将右侧内容转为数字类型

举 🌰 子

1
2
3
4
5
[1] - false // 1
// 等于 '1' - false -> 1 - 0 -> 1

-true
// 等于 -Number(true) -> -1

一些注意点

1
2
NaN == NaN // false
undefined == null // true,这是规定

推荐一些mac上好用的软件

发表于 2019-12-03
| 字数: 1.8k
  • IINA: 算是 mac 上最好的免费本地视频播放器吧,颜值也很高
  • Xnip: 平时一直用它截图 & 取色,免费版够用
  • GIPHY CAPTURE: gif 录制工具
  • iTerm2: 替代本地终端,配合 oh-my-zsh 使用更佳
  • SwitchKey: 自动切换输入法,我设置 vscode、iTerm2 等一些 app 自动切换到英文输入法
  • Smooze: 让 mac 外接第三方鼠标更好用,反转滚动方向、平滑滚动、鼠标手势…,类似软件还有免费的 Mos
  • AirBuddy: 打开 AirPods 后 mac 上也能弹出类似 iPhone 上那种动画了
  • 腾讯柠檬清理: 卸载 app、监控电脑信息、垃圾清理…,不要以为腾讯就是坑,这个很好用
  • Keka: 用来解压缩文件
  • AppCleaner: 卸载 app 用,卸的干净一些,不过现在我都用 腾讯柠檬清理
  • Maipo: 第三方微博客户端,个人用的不多
  • Motrix: 一个开源下载工具,很喜欢它的 ui
  • uPic: 一个图床客户端,功能十分完善而且开源
  • MindNode: 思维导图软件,很流畅
  • Charles: 我用它来抓包,替代 fiddler
  • Proxifier: 这个可以让系统所有软件都走小飞机,注意类似小飞机全局代理和这个不是一个概念
  • Skim: 我用它来看 pdf 书籍,主要是免费
  • Typora: 我用它来看 markdown,目前处于测试版暂时免费
  • Sourcetree: mac 上比较好用的 git 可视化管理工具
  • Transmit: 一个 ftp 工具,作为 xftp 的替代品
  • Dash: 一个 api 文档浏览器和代码片段管理器
  • SnippetsLab: 记个命令还是挺舒服,支持 iCloud 同步,这货已经替代了我的 有道云笔记
  • Showy Edge: mac 上自带输入法不能提示当前是哪个输入法,用了它可以自定一个颜色条在屏幕上
  • Alfred: mac 上我最喜欢的 app,提高了我很多效率,可以替代 聚焦搜索 不说,最主要是 workflow 功能,这里推荐一些
    • Dash: Dash 软件中自带的,更快速搜索 api 文档
    • SnippetsLab: SnippetsLab 软件中自带的,更快速搜索你的笔记
    • CodeVar: 还在为代码中变量取什么发愁吗,它可以自动翻译成多种格式,一定程度上替代了词典
    • Create New File in Finder: mac 上很难受就是没右键创建文件功能,它可以在当前 finder 目录中创建文件
    • Lookup Domain IP: 查看网站 ip 以及延迟,省去你手动 ping 的动作
    • OCR: 总会遇到没法复制文字的情况,它会直接启动截屏程序让你截,然后调用百度 api 识别,几秒就完事
    • QRcode Creator: 生成并展示二维码
    • TerminalFinder: 快速在当前 finder 路径中打开 iTerm2,不得不说 macOS 真是垃圾,啥都得靠第三方
    • Youdao Translate: 有道翻译
    • 切换外观模式: 在黑暗模式与正常模式之间切换

另外推荐一些 alfred 设置

  • 记录粘贴板: 保留粘贴板历史记录,有些时候很有用,Features -> Clipboard History
  • 打开 alfred 时默认切换到某个输入法: Advanced -> Force Keyboard
  • 增加百度搜索: Features -> Web Search,其它不细说,主要 url 设置成 https://www.baidu.com/s?wd={query},同理可以通过这种方式设置为 npm、github 搜索
  • 设置默认搜索: 当你没输入关键字并且没匹配到任何东西时会默认使用谷歌搜索,其实你可以配置此功能。 Features -> Default Results -> Setup fallback results,比如我设置了 百度、谷歌、npm、github、stack overflow,平时直接在 alfred 搜索框中输入关键字,选择搜索类型后会直接使用浏览器打开,这体验比先打开 chrome 再 xxx 强多了
  • 其它: alfred 功能太多了,自行摸索吧,好多功能需要付费,有机会一定入正

js中实现防抖与节流

发表于 2019-11-15
| 字数: 839

防抖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export function debounce(fn, delay) {
let timer = null
return function(...args) {
clearTimeout(timer)
timer = setTimeout(() => {
fn.call(this, ...args)
}, delay)
}
}

// 使用方式
const onResize = debounce(function() {
// ...
}, 300)

节流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export function throttle(fn, delay, cb) {
let time = null
return function(...args) {
const curTime = Date.now()
if (time && curTime - time < delay) {
return typeof cb === 'function' && cb.call(this)
}
time = curTime
fn.call(this, ...args)
}
}

// 使用方式
const query = throttle(
function() {
// ...
},
1000,
function() {
console.log('请求次数过于频繁')
}
)

注意不要使用箭头函数,不然会导致 this 错乱,因为 call 也无法改变箭头函数 this 指向

Vue源码思维导图

发表于 2019-09-19
| 字数: 46

Vue 源码看了近一周,想记录一下大体流程,懒得写文章,直接上图,不是完整版

nLUhZt.png

未完待续

TypeScript中一些被忽视的内置类型

发表于 2019-09-09
| 字数: 2.1k

ts 中其实已经内置了很多常用类型,此处记录一下,不是太完整 🐶

假设我们已经声明了一个 interface

1
2
3
4
interface Person {
name: string
age?: number
}

Partial

将所有参数变成可选

1
2
3
4
5
type Partial<T> = {
[P in keyof T]?: T[P]
}

// Partial<Person> -> { name?: string; age?: number }

Required

将所有参数变成必选

1
2
3
4
5
type Required<T> = {
[P in keyof T]-?: T[P]
}

// Required<Person> -> { name: string; age: number }

Readonly

将所有参数变成只读

1
2
3
4
5
type Readonly<T> = {
readonly [P in keyof T]: T[P]
}

// Readonly<Person> -> { readonly name: string; readonly age?: number }

Pick

挑出一部分属性及声明重新生成一个新类型

1
2
3
4
5
6
type Pick<T, K extends keyof T> = {
[P in K]: T[P]
}

// Pick<Person, 'name'> -> { name: string }
// Pick<Person, 'name' | 'age> -> { name: string; age?: number }

Record

构造一个具有一组属性为 K,类型为 T 的类型

1
2
3
4
5
type Record<K extends keyof any, T> = {
[P in K]: T
}

// Record<'a' | 'b' | 'c', Person> -> { a: Person; b: Person; c: Person; }

Exclude

从 T 中排除那些可赋值给 U 的类型

1
2
3
type Exclude<T, U> = T extends U ? never : T

// Exclude<'admin' | 'guest', 'guest'> -> "admin"

Extract

从 T 中提取那些可赋值给 U 的类型

1
2
3
type Extract<T, U> = T extends U ? T : never

// Extract<'admin' | 'guest', 'guest'> -> "guest"

Omit

用来忽略对象某些属性

1
2
3
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>

// Omit<Person, 'age'> -> { name: string }

NonNullable

从 T 中排除 null 和 undefined

1
2
3
type NonNullable<T> = T extends null | undefined ? never : T

// NonNullable<'a' | 'b' | null> -> "a" | "b"

Parameters

获取函数的参数类型组成的元组类型

1
2
3
4
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never

function foo(a: string, b: number) {}
// Parameters<typeof foo> -> [string, number]

ReturnType

获取函数的返回类型

1
2
3
4
5
6
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any

function foo(x: number): number[] {
return [x]
}
// ReturnType<typeof foo> -> number[]

用travis-ci自动化构建部署github-pages

发表于 2019-09-04
| 字数: 988

前言

最近重新捣鼓了一下以前学习 react 所写的一个后台系统,由于有个在线预览功能使用了 Github Pages,每次 push 前都需要 build 一下,很麻烦,所以想搭建一个 Jekins、GitLab Runner,但是奈何 vps 硬件不给力,索性还是用了 Travis CI。

关联 GitHub 工程

用 github 授权登录 travis,然后把你想关联的工程开关打开(免费版只能用在 github 公共仓库中,私有仓库不行)

编写 .travis.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# .travis.yml
# see https://docs.travis-ci.com/user/deployment/pages/
language: node_js
node_js:
- 10
install:
- yarn
script:
- unset CI
- yarn build
cache: yarn
deploy:
provider: pages
local_dir: build
skip_cleanup: true
keep_history: true
github_token: $GITHUB_TOKEN
on:
branch: master

$GITHUB_TOKEN 指设置里的 Environment Variables,它需要一个 github personal access tokens
nZGZad.jpg

使用 github 生成 token: 打开 https://github.com/settings/tokens -> Generate new token -> 随便填一个名字,勾选 repo 并保存,记好 token,只能看一次
nZG3qg.jpg

新建 gh-pages 分支

由于它默认会 push 到 gh-pages 分支,分支不存在会构建失败,那么我们手动新建一个这个分支

1
2
3
4
5
6
7
git checkout --orphan gh-pages
git rm -rf .

echo '# new branch' >> README.md

git add README.md
git commit -m 'new branch'

检验

push 一下,稍等片刻浏览器中便会打印构建信息

nZBbBq.jpg

记录一下React简易后台管理系统搭建过程

发表于 2019-09-01
| 字数: 13k

最近重新学了一下 react 技术栈,顺便重构了去年一个管理后台,不得不说前端变化实在是太快了,几个月没用 react 代码都快看不懂了。

源码地址 👉https://github.com/xiaojun1994/react-admin-lite

🌵 技术栈

基于 cra ts 版

1
2
3
npx create-react-app my-app --typescript
# or
yarn create react-app my-app --typescript
  • react + hooks
  • react-router
  • mobx
  • typescript
  • antd
  • mockjs

🍭 需求分析

  • antd 按需引入
  • 按配置文件自动创建菜单
  • 路由懒加载
  • 必须登录才能访问,如果没登录直接重定向到登录页面
  • 访问了一个不存在的路由跳转到 404 页面
  • 实现路由级别和按钮级别的权限控制,根据登录用户展示对应菜单
  • 使用 mock 数据
  • mobx 状态持久化,避免刷新丢失状态

🍺 目录

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
├── README.md
├── config-overrides.js rewired配置
├── global.d.ts 存放一些ts全局声明
├── package.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   └── robots.txt
├── src
│   ├── App.test.tsx
│   ├── App.tsx
│   ├── Layout 存放布局组件
│   ├── assets 存放一些图片资源
│   ├── common 存放一些公共变量、方法等
│   ├── components 存放公共组件
│   ├── config 存放一些配置文件
│   ├── index.less
│   ├── index.tsx
│   ├── mock 存放mock数据
│   ├── pages
│   ├── plugins 实际上就放了一个封装后的axios
│   ├── react-app-env.d.ts
│   ├── serviceWorker.ts
│   └── store mobx仓库
├── tsconfig.json
└── yarn.lock

🍬 按需引入 antd

先安装这几个依赖 babel-plugin-import customize-cra less less-loader react-app-rewired

根目录下新建 config-overrides.js 并写入以下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// config-overrides.js
const { override, fixBabelImports, addLessLoader, addDecoratorsLegacy } = require('customize-cra')

module.exports = override(
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: true
}),
addLessLoader({
javascriptEnabled: true,
modifyVars: {
'@primary-color': '#8994DF'
}
}),
addDecoratorsLegacy() // 配置以允许使用装饰器
)

然后别忘了改一下 package.json

1
2
3
4
5
6
7
8
// ...
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
}
// ...

🏀 登录控制

没登录自动跳到登录页面,这个功能通过封装 Route 组件实现,让我们先搞个 AuthRoute。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/components/AuthRoute
import React, { useContext } from 'react'
import { Route, Redirect, RouteProps } from 'react-router-dom'
import * as stores from '../../store'

type PickRequired<T, K extends keyof T> = T & Required<Pick<T, K>>

const AuthRoute: React.FC<PickRequired<RouteProps, 'component'>> = props => {
const { isLogged } = useContext(stores.userStore)
const { component: Component, ...rest } = props

return (
<Route
{...rest}
render={props => {
return isLogged ? <Component {...props} /> : <Redirect to="/login" />
}}
/>
)
}

export default AuthRoute

⚽️ 按配置文件自动生成菜单

以下教程会通过 lazy Suspense 设置路由懒加载

配置文件

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
// global.d.ts
interface IMenu {
name: string // 菜单名
key: string // 菜单对应路由
icon?: string // 菜单图标
component?: React.ComponentType<any> // 菜单路由对应页面
children?: IMenu[] // 子菜单
permissions?: Permission[] // 菜单权限
}
// src/config/menus.ts
import { lazy } from 'react'

const menus: IMenu[] = [
{
name: '合作商户',
key: '/information',
icon: 'user',
component: lazy(() => import('../pages/Information')),
permissions: ['admin']
},
{
name: '订单查询',
key: '/order-query',
icon: 'file-text',
component: lazy(() => import('../pages/OrderQuery')),
permissions: ['admin']
},
{
name: '交易查询',
key: '/trade-query',
icon: 'pay-circle',
component: lazy(() => import('../pages/TradeQuery')),
permissions: ['admin']
},
{
name: '报表导出',
key: '/reports',
icon: 'upload',
component: lazy(() => import('../pages/Reports')),
permissions: ['admin']
},
{
name: '一级菜单',
key: '/level1',
icon: 'man',
children: [
{
name: '二级菜单',
key: '/level2',
icon: 'woman',
children: [
{
name: '三级菜单',
key: '/level3',
icon: 'api',
component: lazy(() => import('../pages/Hello'))
}
]
}
]
},
{
name: '权限测试',
key: '/permission',
icon: 'medicine-box',
component: lazy(() => import('../pages/Permission'))
}
]

export default menus

封装 Menus 组件

Menus 组件负责根据配置文件递归生成侧边菜单

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
// src/components/Menus
import React from 'react'
import { Icon, Menu } from 'antd'
import { ClickParam } from 'antd/lib/menu'
import { withRouter, RouteComponentProps } from 'react-router-dom'
import menus from '../../config/menus'
import { usePermission } from '../../common/hooks'

const Menus: React.FC<RouteComponentProps> = props => {
const { location, history } = props
const hasPermission = usePermission()

function handleNavClick({ key }: ClickParam) {
history.push(key)
}

function hasChild(menu: any) {
return Array.isArray(menu.children) && menu.children.length > 0
}

function genSubMenu(menu: any) {
return (
<Menu.SubMenu
title={
<span>
{menu.icon && <Icon type={menu.icon} />}
<span>{menu.name}</span>
</span>
}
key={menu.key}
>
{genMenus(menu.children)}
</Menu.SubMenu>
)
}

function genMenItem(menu: any) {
return (
<Menu.Item key={menu.key}>
{menu.icon && <Icon type={menu.icon} />}
<span>{menu.name}</span>
</Menu.Item>
)
}

function genMenus(menus: any) {
return menus.reduce((prev: any, next: any) => {
return prev.concat(
hasChild(next)
? hasPermission(next) && genSubMenu(next)
: hasPermission(next) && genMenItem(next)
)
}, [])
}

return (
<Menu theme="dark" mode="inline" selectedKeys={[location.pathname]} onClick={handleNavClick}>
{genMenus(menus)}
</Menu>
)
}

export default withRouter(Menus)

封装 Content 组件

Content 组件负责根据配置文件递归生成路由

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
// src/components/Content
import React, { Suspense } from 'react'
import { Layout, Spin } from 'antd'
import { Switch } from 'react-router-dom'
import AuthRoute from '../AuthRoute'
import menus from '../../config/menus'
import NoMatch from '../../pages/404'
import { usePermission } from '../../common/hooks'

const Content: React.FC = props => {
const hasPermission = usePermission()

function hasChild(menu: any) {
return Array.isArray(menu.children) && menu.children.length > 0
}

function genRoute(menu: any) {
if (!menu.component) return null
return <AuthRoute path={menu.key} component={menu.component} key={menu.key} />
}

function genRoutes(menus: any) {
return menus.reduce((prev: any, next: any) => {
return prev.concat(
hasChild(next)
? hasPermission(next) && genRoutes(next.children)
: hasPermission(next) && genRoute(next)
)
}, [])
}

return (
<Layout.Content
style={{
margin: '24px'
}}
>
<Suspense
fallback={
<div style={{ textAlign: 'center', marginTop: '50px' }}>
<Spin tip="Loading..." />
</div>
}
>
<Switch>
{genRoutes(menus)}
<AuthRoute component={NoMatch} />
</Switch>
</Suspense>
</Layout.Content>
)
}

export default Content

🚁 按钮级别权限控制

上面我们已经实现了菜单级别权限控制,但是这还不够,可能有时候还需要按钮级别权限控制

自定义 hooks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/common/hooks.tsx
import React, { useContext } from 'react'
import { userStore } from '../store'

// ...

// 处理按钮级别权限,类似高阶组件
function useAuthComponent(...permissions: Permission[]) {
const { userinfo } = useContext(userStore)
const hasPermission = permissions.includes(userinfo.permission)

return <P extends {}>(BaseComponent: React.ComponentType<P>): React.FC<P> => props =>
hasPermission ? <BaseComponent {...props} /> : null
}

export { usePermission, useAuthComponent }

这个 hooks 我封装的不是很好,以后看能否优化

使用教程

1
2
3
4
5
6
7
8
9
10
11
12
13
import React from 'react'
import { Button } from 'antd'
import { observer } from 'mobx-react-lite'
import { useAuthComponent } from '../common/hooks'

const Permission: React.FC = props => {
// @ts-ignore 某些组件会报错,忽略它
const AuthButton = useAuthComponent('admin')(Button)

return <AuthButton type="primary">权限为admin该按钮才显示</AuthButton>
}

export default observer(Permission)

🏉 使用 mock 数据

需要安装 mockjs

简单示例,具体请移步官网

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
import Mock from 'mockjs'

Mock.setup({
// 模拟延迟(200-600毫秒之间)
timeout: '200-600'
})

Mock.mock(/login/, (val: any) => {
const isAdmin = val.url.match(/admin/)
return isAdmin
? {
t: {
account: '[email protected]',
name: 'admin',
permission: 'admin'
}
}
: {
t: {
account: '[email protected]',
name: 'guest',
permission: 'guest'
}
}
})

Mock.mock(/getMerchantInfo/, {
t: {
'account|3-8': [
{
name: '@cname',
account: '@email'
}
],
'product|5-10': [
{
'productId|5': /\d/,
'productType|1': [10, 20, 30, 40, 50],
'status|0-1': 0,
'road|5-10': [
{
name: '测试商户',
roadCode: /10021\d{6}/,
'status|0-1': 0
}
]
}
]
}
})

Mock.mock(/order-query/, {
t: {}
})

Mock.mock(/trade-query/, {
't|10': [
{
'id|+1': 0,
merOrderNo: /\d{12}/,
payOrderId: /\d{12}/,
roadCode: /10021\d{6}/,
merName: '@word()',
'amount|10-10000.2-2': 10,
'status|1': [10, 20, 30, 40, 50],
createTime: "@date('yyyy-MM-dd hh:mm:ss')"
}
]
})

之后在 App.tsx 引入它即可

🤡 mobx 持久化存储

很简陋,凑合用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/common/utils.ts
import { autorun } from 'mobx'
/**
* mobx 状态持久化
*/
let is_first_run = true
function mobxPersist<T, K extends keyof T>(store: T, fields: K[]): T {
autorun(() => {
fields.forEach(field => {
if (is_first_run) {
const data = window.sessionStorage.getItem(field as string)
data && (store[field] = JSON.parse(data))
}
window.sessionStorage.setItem(field as string, JSON.stringify(store[field]))
})
is_first_run = false
})
return store
}

export { mobxPersist }

使用教程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { createContext } from 'react'
import { observable, action, computed } from 'mobx'
import { mobxPersist } from '../../common/utils'

export class UserStore {
@observable userinfo: any = {}

@action
saveUserinfo = (data: any) => {
this.userinfo = data
}

@computed
get isLogged() {
return Object.keys(this.userinfo).length > 0
}

@action
cleanUserinfo = () => {
this.userinfo = {}
}
}

export default createContext(mobxPersist(new UserStore(), ['userinfo']))

🌝 结尾

暂时写这么多,感觉还有很大优化空间,等我慢慢完善,ts 给我感觉就像臭豆腐一样,闻着臭,吃着还行~🤖

123
xiaojun1994

xiaojun1994

26 posts
19 tags
GitHub
友链
  • 赖同学
  • John Stark
  • 胡雨
  • mghio
© 2021 xiaojun1994