OverRainbow

official website project review part3

☕️ 3 min read

你可以从本文了解到

帧动画方案的变更

由于 part1 部分提到的“换图的方案”产生的网络请求过多,影响了页面整体的加载。

为此改变了方案,采用了 CSS Sprites 动画。说人话就是 N 张帧动画合成一张大图,然后按顺序换背景图的取图位置。

为此我做了一个工具项目frame-animation-sprite-maker,里面主要是用到了 gka 这个包,使用的方法是把图片放入 images 目录下,运行npm run build:gka会在 build 目录下生成合成的图,之后如果图片尺寸比较大,那么 gka 压缩会出错,我们可以手动采用 imagemin 再压一遍,执行npm run imagemin即可在 dist 目录下产生如下配置的压缩图片啦。

// frame-animation-sprite-maker/imagemin.js
const imagemin = require('imagemin');
// const imageminJpegtran = require('imagemin-jpegtran');
const imageminPngquant = require('imagemin-pngquant');

(async () => {
    const files = await imagemin(['build/**/**.{jpg,png}'], {
        destination: 'dist',
        plugins: [
            // imageminJpegtran(),
            imageminPngquant({
                quality: [0.6, 0.8]
            })
        ]
    });

    console.log(files);
    //=> [{data: <Buffer 89 50 4e …>, destinationPath: 'build/images/foo.jpg'}, …]
})();

而在移动端,这套动画的兼容性还是不够好。为此我们用 Gif 图做了 H5 的动画。CSS 动画还是留给了 PC。

移动端返回位置问题,hook 相关

这个问题主要出现在移动端,用户在返回上一个页面的时候,有一定几率停在错误的地方。据称在 Android 上较为容易出现。为此,只好手动实现一个简单的位置记录器。但逻辑对的,但就是拿不到正确的高度,在页面销毁的时候,总是会发生高度突变。 然后,就想到了 useEffect 是异步入列的,可以试试同步执行的 useLayoutEffect,结果还真好了,执行提前了,高度不会突变了。

import Env from 'Env';
import {useEffect, useLayoutEffect} from 'react';
import {useLocation, useHistory} from 'react-router-dom';
let isFirstLoad = true;
// blocking mode will delay page render, so element will not show immediately
let scrollMap = {};
let lastPathname = '';
export default function ScrollToTopPatch() {
    const {pathname, state} = useLocation();
    const {action, length, location} = useHistory();
    // console.log('ddt::action', action, isFirstLoad);
    let skip = false;
    if (action === 'POP' && !isFirstLoad) {
        // history返回不跳转顶部
        skip = true;
        // fix hooks order change , DON'T return null
    }
    isFirstLoad = false;

    useLayoutEffect(() => {        // console.log('ddt::pathname', pathname);
        // console.log('ddt::action', action, pathname, lastPathname);
        if (action === 'POP') {
            // console.log('ddt::doScroll', action);
            // back?
            if (pathname in scrollMap) {                // pop时,如果有存,则回到记忆的位置
                // console.log('ddt::doScroll', action, scrollMap[pathname]);
                window.scrollTo(0, scrollMap[pathname]);
            }
            return;
        }
        if (skip) return;
        // use search instead of hash when using history router
        const hasJumpSignal = location.search.includes('anchor=');
        if (hasJumpSignal) {
            const anchor: string = location.search
                .split('anchor=')[1]
                .split('&')[0];
            const element: any = document.getElementById(anchor);
            const backScrollOffset = Env.ua.isH5 ? 100 : 200;
            element && window.scrollTo(0, element.offsetTop - backScrollOffset);
        } else {
            // console.log('ddt::top!');
            window.scrollTo(0, 0);
        }
    }, [pathname]);

    useLayoutEffect(() => {        // 此处用useEffect则会读到错误的尺寸
        return () => {
            // console.log('ddt::unmount!', pathname);
            scrollMap[pathname] = window.scrollY; //max; //
            // console.log('ddt::do scrollMap', scrollMap);
            lastPathname = pathname;
        };
    }, []);

    return null;
}

开发效率的相关工具、库

hygen 做代码生成器,统一 components、pages 的模板

storybook 做 UI 组件的独立开发环境(单测环境)

SSR 模式下的 es5 直连调试方法

part1 提到 SPA 模式下的直连方法。但在 SSR 模式下,由于端口占用等问题,就失效了。

其实要解决两个问题:

第一步是如何让双server在代理的情况下run起来。

razzle的ssr开发环境的双server架构,大致如下:

3000=> express server 负责renderToString,流量的直接入口 3001=> webpackDevServer 负责静态文件(默认注入localhost:3001)

而在外部设备如手机端通过proxy连接,将不能解析localhost:3001,因此可以通过如下配置转发。

http://vredu.baidu.com localhost:3000
http://vvredu.baidu.com localhost:3001

这样就完成了端口转发。

第二部是如何编译成es5

第一步完成后,发现外部的js转成了es5,但是一些razzle内部的代码还是es6.

还好我们在 github 上找到这样的实现方法,将这些文件再次进行babel编译。

// razzle.config.js
const ieRule = {
    test: /\.jsx?$/,
    include: new RegExp(
        `node_modules/(?=(${[
            'acorn-jsx',
            'estree-walker',
            'regexpu-core',
            'unicode-match-property-ecmascript',
            'unicode-match-property-value-ecmascript',
            'react-dev-utils',
            'ansi-styles',
            'ansi-regex',
            'chalk',
            'strip-ansi'
        ].join('|')}))`
    ),
    use: {
        loader: 'babel-loader',
        options: {
            presets: [
                [
                    '@babel/preset-env',
                    {
                        targets: {
                            ie: 11
                        }
                    }
                ]
            ]
        }
    }
};
module.exports = {
    plugins: [
        {
            name: 'typescript',
            options: {
                useBabel: false,
                tsLoader: {
                    transpileOnly: true,
                    experimentalWatchApi: true
                },
                forkTsChecker: {
                    tsconfig: './tsconfig.json',
                    tslint: false,
                    watch: './src',
                    typeCheck: true
                }
            }
        }
    ],
    modifyWebpackConfig: ({env: {target, dev}, webpackConfig: config}) => {
        if (dev && target === 'web' && process.env.ECMA === '5') {
            console.log('es5 running');
            // config.entry.client[0] = require.resolve('webpack/hot/dev-server');
            config.module.rules.unshift(ieRule);
        }
    }
};
// .babelrc
{
    "presets": [
      [
        "razzle/babel",
        {
          "targets": {
            "browsers": [
              "ie 11",
              "last 2 Chrome versions",
              "last 2 Firefox versions",
              "last 2 Safari versions"
            ]
          }
        }
      ]
    ]
  }