项目中为了确保页面显示时,图片已经全部加载完毕,因此需要提前加载图片,加载图片的过程使用进度条显示。
在 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')))
整个页面的显示,一共有 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>
- )
- }
每一个 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;
- }
- };
- }
首先定义一个 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;
- }
- }
js 的计算中,经常会遇到小数精度的问题,最初没有注意,导致数据显示出了很多问题。例如如下计算结果
- 1.099 * 100
- 109.89999999999999
解决方法如下:
- (1.099 * 100).toFixed(2)
利用 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);
- }
一次性加载所有图片会导致浏览器 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());
来源: https://segmentfault.com/a/1190000012601310