official website project review part1
• • ☕️ 5 min read帧动画闪烁和浏览器兼容性
分为规则闪烁和不规则闪烁两种。
1.规则闪烁
规则闪烁在不同浏览器表现不一样。chrome、ie、edge 各自有各自的问题。
1、图片删除、插入式的动画方式,在 ie11 上会闪烁。
2、替换图片 src 的动画方式,在 edge 上会闪烁。
3、替换背景图也在 chrome 有不同程度的问题。
摸索下来,选择了方案 2,然后在 edge 上做试验,寻找引起闪烁的原因尝试修复。
目前没有严格准确的结论,但对图片压缩尺寸后,闪烁就消失了(png压缩后损失alpha通道,丢失透明度)。初步结论 edge 渲染效能有问题。
2.不规则闪烁
与浏览器无关,都会发生。在刷新页面或者切页面时容易出现,主要原因是“重入”。
webpack 与调试 es5
参考本文,通过修改 webpack 的 dev server 配置,使得client使用webpack/hot/dev-server,可以实时编译成 es5 给 IE 调试。 提高了调试效率,尤其是IE11。
问:webpack/hot/dev-server怎么知道该编译成 es5?
答:基于package.json的browserslist配置。
源码对应的位置:
// node_modules/razzle/config/createConfig.js
// line 381
config.entry = {
client: [
require.resolve('razzle-dev-utils/webpackHotDevClient'), // 将上面这行替换成require.resolve('webpack/hot/dev-server') paths.appClientIndexJs
]
};
具体步骤:
1.razzle配置化修改
// razzle.config.js
module.exports = {
plugins: [
{
name: 'typescript',
options: {
useBabel: false,
tsLoader: {
transpileOnly: true,
experimentalWatchApi: true
},
forkTsChecker: {
tsconfig: './tsconfig.json',
tslint: false,
watch: './src',
typeCheck: true
}
}
}
],
modify: (config, {target, dev}) => { if (dev && process.env.ECMA === '5') { // 这里ECMA是环境变量控制的功能开关 config.entry.client[0] = require.resolve('webpack/hot/dev-server'); } return config; }};
2.增加带功能开关的脚本,配置正确的browserslist。
// package.json
{
"name": "cra-ts-ssr-zero",
"version": "1.0.0",
"license": "MIT",
"scripts": {
"new:component": "hygen component new",
"new:page": "hygen page new",
"start": "PUBLIC_PATH=./ CLIENT_PUBLIC_PATH=/ razzle start --type=spa",
"start:es5": "PUBLIC_PATH=./ CLIENT_PUBLIC_PATH=/ ECMA=5 razzle start --type=spa", "build": "PUBLIC_PATH=./ razzle build --type=spa"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all",
"chrome >=51"
]
}
IE11 CSS兼容性相关
在做布局的时候用了Grid,ie11不支持,只能转成Flex布局。
const GridContainer: any = styled.div`
// grid-template-columns: 300px 300px 300px;
// grid-template-rows: 200px 200px 200px;
// grid-column-gap: 20px;
// grid-row-gap: 20px;
display: flex;
flex-wrap: wrap;
max-width: 940px;
justify-content: space-between;
`;
const Card: any = styled(Box)<{bg: string}>`
width: 300px; // ++
height: 200px; // ++
position: relative;
background: url(${({bg}) => bg}) no-repeat center;
border-radius: 3px;
overflow: hidden;
:not(:nth-child(-n + 3)) { // ++
margin-top: 20px; // ++
} // ++
`;
关于IE样式的大部分问题,通常可以尝试显式指定宽高。
百度云缓存
bos 和 cdn 资源默认配置了 3 天的强缓存。导致资源更新不能及时生效,建议配置更短的强缓存时间,避免同名资源替换生效时间过长。
PNG 压缩
png 在百度云图像处理压缩后会丢失 alpha 通道,导致一些有部分透明的主色为白的图片变成全白。因此尽量避免压缩 png,只有在背景不是透明的情况下才是相对安全的。
unicode 文本编码
在走查视觉的时候会发现一行字中有两种不同的字体。但看源码中文本并没有什么字体差别。其实文案中存在两种不同范围的 unicode 编码。参见
其中用于部首的 unicode,编码范围是从 U+2F00 到 U+2FD5。
另一种用于常用汉字的编码范围是 U+4E00 到 U+9FFF。
显示字体不同原因:在某些 win10 机器的“雅黑”字库中常用汉字字体有映射,另部首则没有,故采用回落字体(等线)显示。
问题产生原因猜测:文案编写者采用的输入法有问题,未按照常用汉字的规范化编码。
目前 unicode 有:NFD(默认)、NFC、NFKD、NFKC 四种规范化形式。英文和一些部首采用 NFD、NFC,而常用汉字采用 NFKD、NFKC 形式。
解决思路:将部首转换到常用汉字编码范围。比如都转换成 NFKD。
ES6 中提供了工具函数String.prototype.normalize(),可以传入参数"NFKD"。
帧动画实现
可改进的点:减少图片请求数量、采用RAF替代 setTimeout、从可见性方面节能。
代码如下:
/**
* @file [FrameAnimate]
* @author [mzvast]
* @email [mzvast@gmail.com]
* @create date 2020-10-10 17:40:42
*/
/* eslint-disable max-len,babel/new-cap,operator-linebreak,fecs-export-on-declare,space-before-function-paren */
import React, {PureComponent} from 'react';
import {styled, css, keyframes, Box, palette, Flex} from 'galaco';
import getBosPicUrl from 'common/getBosPicUrl';
import {flexCenter} from 'components/sharedStyle';
// borrowed from by https://www.zhangxinxu.com/study/201805/image-sequence-frame-play.html
// 24 fps=> 42ms per img
const Container: any = styled(Box)`
cursor: default;
img {
min-width: 100%;
min-height: 100%;
}
`;
const PuppyContainer: any = styled.img`
flex-shrink: 0;
`;
const TextWrap: any = styled(flexCenter)`
flex-direction: column;
position: absolute;
top: 240px;
width: 100%;
`;
const BigText: any = styled(Box)`
font-family: PingFangSC-Medium;
font-size: 62px;
color: #ffffff;
line-height: 60px;
`;
const SmallText: any = styled(Box)`
font-family: PingFangSC-Regular;
font-size: 18px;
color: #ffffff;
letter-spacing: 0;
line-height: 24px;
margin-top: 30px;
`;
const text = {
big: 'BIG_TEXT',
small: 'SMALL_TEXT'
};
type Props = {
urlRoot: string;
indexRange: number[]; // 动画帧范围eg,[1,143]
visible?: boolean; // 当前状态是否用户可见,用于控制起停
};
type State = {};
class FrameAnimate extends PureComponent<Props, State> {
state: State;
static defaultProps = {};
store = {length: 0};
pendingStore = {length: 0};
maxLength;
eleContainer;
elPuppy;
timer;
isPlaying = false; // flag
lastIndex; // for reset use only
render() {
return (
<Container>
<Flex use={flexCenter} id="container">
<PuppyContainer id="puppy" />
</Flex>
<TextWrap>
<BigText>{text.big}</BigText>
<SmallText>{text.small}</SmallText>
</TextWrap>
</Container>
);
}
componentDidMount() {
this.eleContainer = document.getElementById('container');
this.elPuppy = document.getElementById('puppy');
this.insertPlaceholder();
this.prefetchImg();
}
componentWillUnmount() {
this.timer && clearTimeout(this.timer);
this.clearAllPendingImg();
}
setSrc = (idx, force = false) => {
// idx===0 占位图
if (!force && (this.elPuppy.prevIdx === idx || !this.store[idx])) {
return;
}
let src = this.getOptUrl(
this.props.urlRoot + ('000' + idx).slice(-3) + '.png'
);
// console.log('ddt::setSrc,idx', idx);
this.elPuppy.src = src;
this.elPuppy.prevIdx = idx;
};
// 不建议使用cwrp接口 componentDidUpdate(prevProps, prevState) { // console.log(
// 'ddt::prevProps.visible,this.props.visible',
// prevProps.visible,
// this.props.visible
// );
if (!prevProps.visible && this.props.visible) {
this.play();
} else {
this.reset();
}
}
getOptUrl = (url) => {
return getBosPicUrl.frame(url);
};
// 占位图
insertPlaceholder = () => {
// may have some latency due to network speed
const {indexRange, urlRoot} = this.props;
this.setSrc(0, true);
};
/**
* 图片预加载task manage相关-start
* */
addPendingImg = (idx, img) => {
this.pendingStore[idx] = img;
};
removePendingImg = (idx) => {
if (this.pendingStore[idx]) {
delete this.pendingStore[idx];
}
};
clearAllPendingImg = () => {
for (const idx in this.pendingStore) {
if (Object.prototype.hasOwnProperty.call(this.pendingStore, idx)) {
if (this.pendingStore[idx].src) {
this.pendingStore[idx].onload = null;
this.pendingStore[idx].onerror = null;
this.pendingStore[idx].src = '';
}
this.removePendingImg(idx);
}
}
};
/**
* 图片预加载task manage相关-end
* */
// 预加载图片到内存
prefetchImg = () => {
const {indexRange, urlRoot} = this.props;
this.maxLength = indexRange[1] - indexRange[0] + 1;
for (let idx = indexRange[0]; idx <= indexRange[1]; idx++) {
const img = new Image();
let src = this.getOptUrl(
urlRoot + ('000' + idx).slice(-3) + '.png'
);
img.onload = () => {
this.store.length++;
// 存储预加载的图片对象
this.store[idx] = img;
img.src && this.play();
this.removePendingImg(idx);
};
img.onerror = () => {
this.store.length++;
img.src && this.play();
this.removePendingImg(idx);
};
img.src = src;
this.addPendingImg(idx, img);
}
};
play = () => {
const {indexRange, urlRoot} = this.props;
const percent = Math.round((100 * this.store.length) / this.maxLength);
// 预加载完毕后开始动画,防重入
if (percent == 100 && !this.isPlaying) {
this.isPlaying = true;
let index = indexRange[0];
// 依次append图片对象
const step = () => {
if (!this.isPlaying || !this.props.visible) {
return;
}
this.setSrc(index);
this.lastIndex = index; // remember last inserted index
// 序列增加
index++;
// 如果超过最大限制
if (index <= indexRange[1]) {
this.timer = setTimeout(step, 42);
} else {
// 本段播放结束回调
this.reset();
}
};
// 等100%动画结束后执行播放
this.timer = setTimeout(() => {
step();
}, 100);
}
};
// 重置
reset = () => {
if (!this.isPlaying) return;
this.isPlaying = false;
const {indexRange, urlRoot} = this.props;
this.setSrc(this.lastIndex);
this.timer && clearTimeout(this.timer);
};
}
export default FrameAnimate;