为何写这个文章
很少有时间对于写过的代码重构 这次发现重构是真的很有意思的事情 所以就记录下来了
modal的fifo应用
集卡属于互动类型的游戏,此页面有9个弹窗,其中有同时出现的5个弹窗的情况,且如果同时出现必须按照指定顺序弹出。
遇到复杂的交互逻辑,数据结构可以帮助理清思路,抽象逻辑,完成稳定可靠的代码。在这次交互中,弹框要一个个按照顺序弹出,可以虑有序队列。但是弹框的弹出和关闭属于事件。在上一个弹框弹出关闭后,触发下一个弹框弹出。可以考虑事件的发布订阅。
队列图解

弹窗和队列结合
队列的弹窗展示有三个状态
- 同一时间段只有一个弹窗触发
- 同一时间段有两个或多个弹窗出发
- 一个弹窗在展示过程中,另一个弹窗要触发
整体逻辑分析如下图

import { EventEmitter } from "events"
const emitter = new EventEmitter()
const queue = new Queue()
// 事件中心
export class EventCenter {
// 添加绑定事件
static on(type, cb) {
this[`${type}`] = cb
emitter.on(type, this[`${type}`])
}
// 执行绑定事件回掉
static emit(type, data) {
emitter.emit(type, data)
// 每次回掉后 就接触绑定
if (type.indexOf("Close") > -1) {
this.remove(type)
}
}
// 解除绑定事件
static remove(type) {
emitter.removeListener(type, this[`${type}`])
}
static eventNames() {
return emitter.eventNames()
}
}
// 一个弹窗的队列
class Queue {
constructor() {
this.dataStore = []
}
enqueue(e) {
this.dataStore.push(e)
}
dequeue() {
this.dataStore.shift()
}
front() {
return this.dataStore[0]
}
back() {
return this.dataStore[this.dataStore.length - 1]
}
isEmpty() {
if (this.dataStore.length === 0) {
return true
}
return false
}
toString() {
return this.dataStore.join(",")
}
}
/**
* 将弹窗事件名推入队列
*/
export const push = eventName => {
if (queue.isEmpty()) {
queue.enqueue(eventName)
openDialog() // 启动出队逻辑
} else {
queue.enqueue(eventName) // 循环中依然可以同时入队新的元素
}
}
/**
* 打开弹窗,递归,循环出队
*/
const openDialog = () => {
// 打开弹窗
EventCenter.emit(queue.front())
// 监听弹窗关闭
EventCenter.on(`${queue.front()}Close`, () => {
queue.dequeue() // 出队
if (!queue.isEmpty()) {
// 队列不为空时,递归
openDialog()
}
})
}
结果

创建enum管理状态
游戏状态多意味着常量多,好的代码最好是不写注释也一目了然。如果可以把注释通过代码表示出来那就太棒了,限制维护者强制书写注释那就更好了。
实现
// 创建enum 数据
/** *
* 数据类型
* KEY:{
* value:string,
* desc:string
* }
*
* enum[KEY] => value:string
* enum.keys() => KEYS: Array<KEY>
* enum.values() => values: Array<value>
*
*/
export const createEnum = enumObj => {
const keys = target => Object.keys(target) || []
const values = target =>
(Object.keys(target) || []).map(key => {
if (
typeof target[key] === "object" &&
target[key].hasOwnProperty("value")
) {
return target[key].value
} else if (
typeof target[key] === "object" &&
!target[key].hasOwnProperty("value")
) {
return key
}
})
const handler = {
get: function(target, key) {
switch (key) {
// keys 获取key => Array<key:string>
case "keys":
return () => keys(target)
// values Array<key:string> || Array<Object<key:string,value:string>>
case "values":
return () => values(target)
// 获取 详细描述 descs TODO
// [key,[value]] 键值对 entries TODO
// 合并 assign TODO
default:
break
}
if (target[key]) {
if (
typeof target[key] === "object" &&
target[key].hasOwnProperty("value")
) {
return target[key].value
} else if (
typeof target[key] === "object" &&
!target[key].hasOwnProperty("value")
) {
return key
}
return target[key]
}
throw new Error(`No such enumerator: ${key}`)
},
}
return new Proxy(enumObj, handler)
}
使用
export const MODAL_TYPE = createEnum({
GIVE_CARD: {
value: "giveCard",
desc: "天降弹窗",
},
ASSIST_CARD: {
value: "assistCard",
desc: "助力弹窗",
},
BUY_CARD: {
value: "buyCard",
desc: "下单弹窗",
},
TASK_CARD: {
value: "taskCard",
desc: "任务弹窗",
},
RECEIVE_CARD: {
value: "receiveCard",
desc: "收卡弹窗",
},
SEND_CARD: {
value: "sendCard",
desc: "送卡弹窗",
},
})
代码抽离和模块划分
优化后的代码index.js减少了500行左右的代码
我的处理是
- 先进行模块划分,把this.renderXXX里的代码放入components里让整体逻辑分离更清晰,把只和子组建相关的逻辑全部移除到子组建中
- 首页的ajax多个请求合并,把index.js和store内都会用到的请求都组合再一起,对整体页面init的逻辑整合
- 此时再整理首页里面的各种 状态控制锁。将状态控制锁放入constant里。只需要根据不同状态进入不同的组建即可,进行整体的逻辑整合。因为原来的逻辑是 user的状态有4中,活动的状态有4中。这样的排列组合就有16中。需要把这么多排列组合分别在首页逻辑中判断是很复杂的。这里可以考虑根据展示结果判断。同样的展示结果对应哪几种user和activity状态判断组合在一起。后期增加游戏状态时候,只需要再加一个状态和对应的组建即可,也无惧user和活动状态的多变性
- 分离出ui组建和容器组建
- UI组件:只负责页面的渲染
- 容器组件:只负责业务逻辑和数据的处理,要保证组建功能的完整性,与父组建的交互只需给出回调的callback。与父组建无关的逻辑就封闭起来。
- 对觉得已经不满足后期维护需求,承载负荷过重的代码进行重构。此时代码已经解耦,重构起来会是一件快乐和容易的事情
- 对css,动画等进行优化
- 删除重复和多余的代码
- 分析页面性能,进行具体调整
单测
写单测是和留下组建快照 是代码的一次快照缩影,以后维护中可以验证是否修改最后的html结构。用代码描述出最基本的功能,并进行验证。这样保证代码的设计合理性和容易理解的实用性。
import {
createEnum,
} from "../../../activity/collect_card/utils"
describe("createEnum", () => {
const mockData = {
GET: {
value: "get",
desc: "test",
},
POST: "post",
}
it("get value ", () => {
const data = createEnum(mockData)
expect(data.GET).toBe("get")
expect(data.POST).toBe("post")
})
it("object.keys() run success", () => {
const data = createEnum(mockData)
expect(data.keys()).toEqual(["GET", "POST"])
})
it("object.values() run success", () => {
const data = createEnum(mockData)
expect(data.values()).toEqual(["get", "post"])
})
})
加载优化
骨架屏
import React, { useMemo } from "react"
import classNames from "classnames"
import PropTypes from "prop-types"
import "./index.less"
function toPoint(percent) {
if (!`${percent}`.split("").includes("%")) {
return percent
}
var str = percent.replace("%", "")
str = str / 100
return str
}
function isPercent(percent) {
return `${percent}`.split("").includes("%")
}
const SkeletonItem = props => {
const {
className,
component: Component = "span",
height = 16,
variant = "text",
width,
style,
children,
...rest
} = props
const clientWidthCalc = document.documentElement.clientWidth / 360
const getBackgroundSize = useMemo(() => {
if (variant !== "img") {
return {}
}
if (!isPercent(width))
return { backgroundSize: `${(width / 2) * clientWidthCalc}px` }
return {
backgroundSize: `${((toPoint(`${width}`) * 360) / 2) *
clientWidthCalc}px`,
}
}, [width, variant, clientWidthCalc])
return (
<Component
className={classNames("root", variant, className)}
style={Object.assign(
{
width: isPercent(width) ? width : `${width * clientWidthCalc}px`,
height: isPercent(height) ? height : `${height * clientWidthCalc}px`,
...style,
},
getBackgroundSize,
{}
)}
{...rest}
>
{children}
</Component>
)
}
SkeletonItem.propTypes = {
// class名称
className: PropTypes.string,
// 组建类型 span div
component: PropTypes.elementType,
// 组建高度 40% 48 百分比一般用于文本
height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
// 元素形状 text 文本 rect 长方形 circle 圆形 img图片
variant: PropTypes.oneOf(["text", "rect", "circle", "img"]),
// 组建宽度 40% 48
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
}
export default SkeletonItem
@skeleton_bg_color: #f8f8f8;
@skeleton_bg_color_2: #f2f2f2;
.root {
display: block;
background-color: @skeleton_bg_color;
}
// 文本
.text {
position: relative;
display: block;
height: 1.2em;
min-height: 18 * @rex;
border-radius: 4 * @rex;
overflow: hidden;
background-color: @skeleton_bg_color;
transform: scale(1, .6);
transform-origin: 0 60%;
}
.text:empty::after {
position: absolute;
display: block;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
rgba(248, 248, 248, 0),
@skeleton_bg_color_2 75%,
rgba(248, 248, 248, 0)
);
transform: translateX(-100%);
animation: skeletons-loading 2s infinite ease-out;
content: " \00a0";
}
// 圆
.circle {
border-radius: 50%;
}
.rect {
position: relative;
border-radius: 4 * @rex;
overflow: hidden;
background-color: @skeleton_bg_color;
}
.rect:empty::after {
position: absolute;
display: block;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
rgba(248, 248, 248, 0),
@skeleton_bg_color_2 75%,
rgba(248, 248, 248, 0)
);
transform: translateX(-100%);
animation: skeletons-loading 2s infinite ease-out;
content: " \00a0";
}
// 图片
.img {
background: url(../../../../images/default-image.png) center center no-repeat;
background-color: #f8f8f8;
}
@-webkit-keyframes skeletons-loading {
100% {
transform: translateX(100%);
}
}
@keyframes skeletons-loading {
100% {
transform: translateX(100%);
}
}
.goods_list_skeleton {
min-height: 100vh;
padding-top: 21 * @rex;
background: #fff;
.title {
margin: 0 auto 21 * @rex;
}
.list {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding: 0 3 * @rex;
.item {
width: 175 * @rex;
margin-bottom: 13 * @rex;
}
}
}
使用
const SkeletonGoodsList = () => {
return (
<div className="goods_list_skeleton">
<Item variant="rect" width={170} height={18} className="title"></Item>
<div className="list">
{Array.from(
[0, 1, 2, 3, 4, 5].map(item => (
<div className="item" key={item}>
<Item variant="img" width={175} height={175}></Item>
<Item variant="text"></Item>
<Item variant="text"></Item>
<Item variant="text" width={"40%"}></Item>
</div>
))
)}
</div>
</div>
)
}
修改css
使用stylelint检测css一些不和规则的书写
因为css里有一些属性会触发重绘 所以 按照一定的书写顺序可以减少重绘提高css加载速度
而使用简写等可以减少css的打包体积
1. 安装
npm install stylelint --save-dev
npm install stylelint-config-standard --save-dev
npm install stylelint-order --save-dev
2. 在根目录下创建.stylelintrc配置文件
{
"extends": "stylelint-config-standard",
"plugins": ["stylelint-order"],
"rules": {
"order/order": [
"declarations",
"custom-properties",
"dollar-variables",
"rules",
"at-rules"
],
"order/properties-order": [
"position",
"z-index",
"top",
"bottom",
"left",
"right",
"float",
"clear",
"columns",
"columns-width",
"columns-count",
"column-rule",
"column-rule-width",
"column-rule-style",
"column-rule-color",
"column-fill",
"column-span",
"column-gap",
"display",
"grid",
"grid-template-rows",
"grid-template-columns",
"grid-template-areas",
"grid-auto-rows",
"grid-auto-columns",
"grid-auto-flow",
"grid-column-gap",
"grid-row-gap",
"grid-template",
"grid-template-rows",
"grid-template-columns",
"grid-template-areas",
"grid-gap",
"grid-row-gap",
"grid-column-gap",
"grid-area",
"grid-row-start",
"grid-row-end",
"grid-column-start",
"grid-column-end",
"grid-column",
"grid-column-start",
"grid-column-end",
"grid-row",
"grid-row-start",
"grid-row-end",
"flex",
"flex-grow",
"flex-shrink",
"flex-basis",
"flex-flow",
"flex-direction",
"flex-wrap",
"justify-content",
"align-content",
"align-items",
"align-self",
"order",
"table-layout",
"empty-cells",
"caption-side",
"border-collapse",
"border-spacing",
"list-style",
"list-style-type",
"list-style-position",
"list-style-image",
"ruby-align",
"ruby-merge",
"ruby-position",
"box-sizing",
"width",
"min-width",
"max-width",
"height",
"min-height",
"max-height",
"padding",
"padding-top",
"padding-right",
"padding-bottom",
"padding-left",
"margin",
"margin-top",
"margin-right",
"margin-bottom",
"margin-left",
"border",
"border-width",
"border-top-width",
"border-right-width",
"border-bottom-width",
"border-left-width",
"border-style",
"border-top-style",
"border-right-style",
"border-bottom-style",
"border-left-style",
"border-color",
"border-top-color",
"border-right-color",
"border-bottom-color",
"border-left-color",
"border-image",
"border-image-source",
"border-image-slice",
"border-image-width",
"border-image-outset",
"border-image-repeat",
"border-top",
"border-top-width",
"border-top-style",
"border-top-color",
"border-top",
"border-right-width",
"border-right-style",
"border-right-color",
"border-bottom",
"border-bottom-width",
"border-bottom-style",
"border-bottom-color",
"border-left",
"border-left-width",
"border-left-style",
"border-left-color",
"border-radius",
"border-top-right-radius",
"border-bottom-right-radius",
"border-bottom-left-radius",
"border-top-left-radius",
"outline",
"outline-width",
"outline-color",
"outline-style",
"outline-offset",
"overflow",
"overflow-x",
"overflow-y",
"resize",
"visibility",
"font",
"font-style",
"font-variant",
"font-weight",
"font-stretch",
"font-size",
"font-family",
"font-synthesis",
"font-size-adjust",
"font-kerning",
"line-height",
"text-align",
"text-align-last",
"vertical-align",
"text-overflow",
"text-justify",
"text-transform",
"text-indent",
"text-emphasis",
"text-emphasis-style",
"text-emphasis-color",
"text-emphasis-position",
"text-decoration",
"text-decoration-color",
"text-decoration-style",
"text-decoration-line",
"text-underline-position",
"text-shadow",
"white-space",
"overflow-wrap",
"word-wrap",
"word-break",
"line-break",
"hyphens",
"letter-spacing",
"word-spacing",
"quotes",
"tab-size",
"orphans",
"writing-mode",
"text-combine-upright",
"unicode-bidi",
"text-orientation",
"direction",
"text-rendering",
"font-feature-settings",
"font-language-override",
"image-rendering",
"image-orientation",
"image-resolution",
"shape-image-threshold",
"shape-outside",
"shape-margin",
"color",
"background",
"background-image",
"background-position",
"background-size",
"background-repeat",
"background-origin",
"background-clip",
"background-attachment",
"background-color",
"background-blend-mode",
"isolation",
"clip-path",
"mask",
"mask-image",
"mask-mode",
"mask-position",
"mask-size",
"mask-repeat",
"mask-origin",
"mask-clip",
"mask-composite",
"mask-type",
"filter",
"box-shadow",
"opacity",
"transform-style",
"transform",
"transform-box",
"transform-origin",
"perspective",
"perspective-origin",
"backface-visibility",
"transition",
"transition-property",
"transition-duration",
"transition-timing-function",
"transition-delay",
"animation",
"animation-name",
"animation-duration",
"animation-timing-function",
"animation-delay",
"animation-iteration-count",
"animation-direction",
"animation-fill-mode",
"animation-play-state",
"scroll-behavior",
"scroll-snap-type",
"scroll-snap-destination",
"scroll-snap-coordinate",
"cursor",
"touch-action",
"caret-color",
"ime-mode",
"object-fit",
"object-position",
"content",
"counter-reset",
"counter-increment",
"will-change",
"pointer-events",
"all",
"page-break-before",
"page-break-after",
"page-break-inside",
"widows"
],
"no-empty-source": null,
"property-no-vendor-prefix": [true, {"ignoreProperties": ["background-clip"]}],
"number-leading-zero": "never",
"number-no-trailing-zeros": true,
"length-zero-no-unit": true,
"value-list-comma-space-after": "always",
"declaration-colon-space-after": "always",
"value-list-max-empty-lines": 0,
"shorthand-property-no-redundant-values": true,
"declaration-block-no-duplicate-properties": true,
"declaration-block-no-redundant-longhand-properties": true,
"declaration-block-semicolon-newline-after": "always",
"block-closing-brace-newline-after": "always",
"media-feature-colon-space-after": "always",
"media-feature-range-operator-space-after": "always",
"at-rule-name-space-after": "always",
"indentation": 2,
"no-eol-whitespace": true,
"string-no-newline": null
}
}
3. 使用
npx stylelint "**/*.css" --fix // fix 是自动修改
4. stylelint的检测提示
下图 是stylelint检测的要修改的点 加入--fix就会自动修改了

对比 虽然很小 但是 量变产生质变 文件多了自然就减少的多了


bundle区分 使用react lazy
const RedBagRain = React.lazy(() =>
import(
/* webpackPrefetch: true, webpackChunkName: 'RedBagRain.lazy'*/ "./components/red_bag_rain"
)
)
<React.Suspense fallback={null}>
<RedBagRain
dis_cold_start_selectors={[".mask", ".slidemodal"]}
visible={!showFinishPage}
/>
</React.Suspense>
RedBagRain有155 kb ,组建单独打包,分包加载,最后显示分出来 我把modal,actionsheet等 需要点击才能显示出的组建分包加载。这样就达到了首要的包优先加载,其他的包懒加载。如果不需要立即加载的,延迟1000s加载的写法是
const RedBagRain = React.lazy(() => {
return new Promise(resolve => setTimeout(resolve, 10 * 100)).then(() =>
import(/*webpackChunkName: 'RedBagRain.lazy'*/ "./components/red_bag_rain")
)
})
图片压缩
点击进入tiny-图片压缩网站 webpack-bundle-analyzer 分析那些图片太大,如果不是主要图片就进行再次压缩
结果对比
打包结果对比:

优化前

加载结果对比

