1. 借助require.context
预加载图片
项目中为了确保页面显示时,图片已经全部加载完毕,因此需要提前加载图片,加载图片的过程使用进度条显示。
在webpack构建的项目中,可以使用require.context
来获取到静态资源的地址。语法如下:
require.context(directory, useSubdirectories = false, regExp = /^\.\//)
第一个参数表示要搜索的文件夹目录,该目录支持相对路径与在配置文件中定义的路径别名。
第二个参数表示是否搜索其子目录。
第三个参数是一个用来匹配文件的正则表达式。
require.context('modules/App', true, /\.(png|jpg|jpeg|gif)$/);
// 创建一个包含App目录下所有图片的上下文模块
可以使用该上下文模块自带的keys方法得到路径组成的数组。
const images = require.context('modules/App/', true, /\.(png|jpeg|jpg|gif)$/);
console.log(images.keys());
效果大概如下图所示。
得到图片路径之后,就可以借助Promise.all
来完成图片预加载,确保图片加载完成之后再渲染页面。
Promise.all(images.keys().map(path => {
const image = new Image();
image.src = path;
image.onload = image.onerror = () => {
resolve();
}
}))
但是在开发中遇到一个问题,本地页面引用的图片是编译过后的图片地址,并不是相对路径,因此如果直接这样的话会因此地址不一致而报错。
解决办法是在设置image对象src属性时,修改如下:
image.src = images(path);
// images 是由require.context 创建的上下文模块
打印出images(path)
之后的图片路径如下:
上面的修复方式可以使用如下的知识点来理解。
const ctx = require.context('modules/App', true, /*\.js/);
const table = ctx('./table.js');
// 上面的代码等价于
const table = require('modules/App/table.js'); // 使用require引入模块
当还需要从服务端提前加载其他资源时,可以使用数组的concat
方法一起放入Promise.all
中。
Promise.all(images.keys().map(
// ...
).concat(http.get('/api/v1/summary')))
2. 细节优化
整个页面的显示,一共有15页构成,由于每一页的逻辑与效果都有不少差异,因此将每一页定义为了一个组件,最初在引入这些模块时很糟糕的这样做:
import Page00 from './Page00';
import Page01 from './Page01';
import Page02 from './Page02';
import Page03 from './Page03';
import Page04 from './Page04';
import Page05 from './Page05';
import Page06 from './Page06';
import Page07 from './Page07';
import Page08 from './Page08';
import Page09 from './Page09';
import Page10 from './Page10';
import Page11 from './Page11';
import Page12 from './Page12';
import Page13 from './Page13';
import Page14 from './Page14';
// render里也很复杂
// ...
render() {
return (
<Fragment>
<Page00 />
<Page01 />
<Page02 />
...
<Page14 />
</Fragment>
)
}
当组件更多时,这样的引入方式自然是不合理的,可以使用循环的方式来引入代码,优化如下:
const allPages = [];
for(let i = 0; i < 15; i++) {
const id = `0${i}`.slice(-2);
allPages.push(require(`./Page${id}`).default)
}
这样就将所有的Page组件放在了allPages
数组中。
render里也可以使用map来渲染。
render() {
return (
<div className="pages">
{allPages.map(({ id, Component: Page }) => <Page key={id} {...other} />)}
</div>
)
}
3. 使用高阶组件处理公共逻辑
每一个Page组件中,都有共同的元素或逻辑,包括logo,分享当前屏幕截图按钮,统计逻辑,判断对应页面是否显示等。可以将这些共用逻辑使用高阶组件来处理以简化代码。
因此定义了withBox
组件来处理它们。
import React from 'react';
import logo from './images/logo.png';
import { sendEvent } from 'utils/track';
import share from './share';
export default function(Wrapped, checkProp) {
return class NewPage extends React.Component {
shareScreen = () => {
const id = this.refs.box.getAttribute('data-page-id');
this.refs.box.classList.add('will-screenshot');
setTimeout(() => share.shareScreenshot(), 100);
setTimeout(() => this.refs.box.classList.remove('will-screenshot'), 1500);
sendEvent('share-click', 'page' + id);
sendEvent('click', 'share-btn');
};
render() {
const { id, className, ...props } = this.props;
const cls = className ? `page${id} ${className}` : `page${id}`;
if (!checkProp || (props.info[checkProp] !== null && props.info[checkProp] !== 'undefined')) {
return (
<section className={cls} data-page-id={id} ref="box">
<Wrapped {...props} />
<button className="share-btn aninode fadeIn" onClick={this.shareScreen} />
<img className="logo aninode fadeIn" src={logo} alt="tigerbrokers" />
</section>
);
}
return null;
}
};
}
4. 通过添加/删除元素的方式统一控制动画
首先定义一个class如下,将会参与动画的元素(或其父级)都添加该class以隐藏。
.aninode {
visibility: hidden;
}
并在同元素(或父级)添加了animated
时,元素显示。
.animated {
&.aninode, .aninode {
visibility: visible;
}
}
并在运动元素的class中添加了animated
时,运动生效,因此定义运动css时,应该这样做:
.animated {
&.flyTopIn, .flyTopIn {
animation-name: flyTopIn;
animation-duration: 1s;
}
/* more */
}
因此,运动元素在运动开始之前,应该保持这样
<div class="test aninode flyTopIn"></div>
需要运动时,在该元素的class中添加animated
即可。
<div class="test aninode flyTopIn animated"></div>
// or
<div class="animated">
<div class="test aninode flyTopIn"></div>
</div>
使用sass的循环语法定义delay样式
@for $i from 0 through $delay_count {
.animated .delay#{$i * 100} {
animation-delay: $i * 100;
animation-fill-mode: backwards;
}
}
5. 小数精度问题导致的bug
js的计算中,经常会遇到小数精度的问题,最初没有注意,导致数据显示出了很多问题。例如如下计算结果
1.099 * 100
109.89999999999999
解决方法如下:
(1.099 * 100).toFixed(2)
6. Promise与setTimeout的写法问题
利用setTimeout
判断某个对象是否注入成功。
// 错误写法
export const checkSDK = () => {
var timer = null;
const start = Date.now();
return new Promise((resolve, reject) => {
if (typeof window.TigerBridge === 'object') {
resolve();
return;
}
if (Date.now() - start <= 5 * 1000) {
clearTimeout(timer);
timer = setTimeout(checkSDK, 100);
return;
}
reject();
})
}
// 正确写法
export const checkBridge = () => {
var timer = null;
const start = Date.now();
function check(resolve, reject) {
if (typeof window.TigerBridge === 'object') {
resolve();
return true;
}
if (Date.now() - start <= 5 * 1000) {
clearTimeout(timer);
timer = setTimeout(check.bind(null, resolve, reject), 100);
return;
}
reject();
return false;
}
return new Promise((resolve, reject) => check(resolve, reject))
}
本地模拟注入过程
if (process.env.NODE_ENV != 'production') {
setTimeout(() => {
window.TigerBridge = {
getAccessToken: () => {
return pkg.token;
},
isAccountPermissionLimited: () => false
};
}, 1600);
}
7. 图片串行加载优化
一次性加载所有图片会导致浏览器http线程阻塞严重。因此需要稍作优化,让图片一张一张加载。
// 优化前
images.keys().map(path => new Promise(resolve => {
const image = new Image();
image.src = images(path);
image.onload = image.onerror = resolve;
}))
// 优化后
images.keys().reduce((cachePromise, path) => cachePromise.then(() => {
return new Promise(resolve => {
const image = new Image();
const complete = () => {
clearTimeout(timer);
resolve();
}
const timer = setTimeout(complete, 1000); // 单张图片最多加载1s
image.src = images(path);
image.onload = image.onerror = complete;
})
}), Promise.resolve());
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。