Giter Site home page Giter Site logo

gwl002.github.io's People

Contributors

gwl002 avatar

Watchers

 avatar  avatar

gwl002.github.io's Issues

react-native-webview加载第三方iframe

1. 第三方iframe 遇到cross-origin问题

设置baseUrl与iframe的domain相同

const ChatModal = ({ uuid, token, language, isLogin, dispatch, chatModal }) => {
    const { shown } = chatModal;
    // const [isLoading, setIsLoading] = useState(false);

    const closeModal = () => {
        dispatch({
            type: 'modal/closeChatModal',
        });
    };

    const handleMessage = ({ nativeEvent }) => {
        const data = nativeEvent.data;
        if (data === 'close') {
            closeModal();
        }
    };

    return (
        <Modal animationType="fade" transparent={false} visible={shown}>
            <SafeAreaView style={styles.container}>
                <WebView
                    source={{ html: html, baseUrl: 'https://chat.sleekflow.io/' }}
                    style={styles.webview}
                    onMessage={handleMessage}
                />
            </SafeAreaView>
        </Modal>
    );
};

2. 通过postMessage 向rn发送event

rootWindow.ReactNativeWebView.postMessage('close'); 

3. react-native中调试js

3.1 ios中调试js

  1. Open Safari Preferences -> "Advanced" tab -> enable checkbox "Show Develop menu in menu bar"
  2. Start app with React Native WebView in iOS simulator or iOS device
  3. Safari -> Develop -> [device name] -> [app name] -> [url - title]
  4. You can now debug the WebView contents just as you would on the web

3.2 android中调试js

  1. You will need to make the following change to MainApplication.java to enabled web contents debugging:
import android.webkit.WebView;

  @Override
  public void onCreate() {
    super.onCreate();
	  ...
    WebView.setWebContentsDebuggingEnabled(true);
  }
  1. Start app with React Native WebView in Android emulator or Android device
  2. Open chrome://inspect/#devices on Chrome
  3. Select your device on the left and select "Inspect" on the WebView contents you'd like to inspect
  4. You can now debug the WebView contents just as you would on the web

4. html source code

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
        <style>
            html,body{
                margin:0;
                padding:0
            }
            #loading-overlay{
                position:absolute;
                width:100%;
                height:100vh;
                display: flex;
                align-items: center;
                justify-content: center;
                z-index:2147483647;
                background-color:white;
            }
            .lds-spinner {
            color: official;
            display: inline-block;
            position: relative;
            width: 80px;
            height: 80px;
            transform:scale(0.8)
            }
            .lds-spinner div {
            transform-origin: 40px 40px;
            animation: lds-spinner 1.2s linear infinite;
            }
            .lds-spinner div:after {
            content: " ";
            display: block;
            position: absolute;
            top: 3px;
            left: 37px;
            width: 6px;
            height: 18px;
            border-radius: 20%;
            background: blue;
            }
            .lds-spinner div:nth-child(1) {
            transform: rotate(0deg);
            animation-delay: -1.1s;
            }
            .lds-spinner div:nth-child(2) {
            transform: rotate(30deg);
            animation-delay: -1s;
            }
            .lds-spinner div:nth-child(3) {
            transform: rotate(60deg);
            animation-delay: -0.9s;
            }
            .lds-spinner div:nth-child(4) {
            transform: rotate(90deg);
            animation-delay: -0.8s;
            }
            .lds-spinner div:nth-child(5) {
            transform: rotate(120deg);
            animation-delay: -0.7s;
            }
            .lds-spinner div:nth-child(6) {
            transform: rotate(150deg);
            animation-delay: -0.6s;
            }
            .lds-spinner div:nth-child(7) {
            transform: rotate(180deg);
            animation-delay: -0.5s;
            }
            .lds-spinner div:nth-child(8) {
            transform: rotate(210deg);
            animation-delay: -0.4s;
            }
            .lds-spinner div:nth-child(9) {
            transform: rotate(240deg);
            animation-delay: -0.3s;
            }
            .lds-spinner div:nth-child(10) {
            transform: rotate(270deg);
            animation-delay: -0.2s;
            }
            .lds-spinner div:nth-child(11) {
            transform: rotate(300deg);
            animation-delay: -0.1s;
            }
            .lds-spinner div:nth-child(12) {
            transform: rotate(330deg);
            animation-delay: 0s;
            }
            @keyframes lds-spinner {
            0% {
                opacity: 1;
            }
            100% {
                opacity: 0;
            }
            }
        </style>
    </head>
    <body>
        <div id="loading-overlay">
            <div class="lds-spinner"><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div>
        </div>
    </body>
    <script 
        src="https://chat.sleekflow.io/embed_iframe.js" 
        data-id="travischatwidget"
        data-companyid="e1922105-8004-4724-97c7-859b7ff5dedf"
        type="text/javascript"
    >
    </script>
    <script>
        const rootWindow = window;
        const loadingEle = document.querySelector('#loading-overlay') //loading

        function start(){
            function checkLoad(iframe){
                const iframeWindow = iframe.contentWindow.window;
                const rootIframeEle = iframeWindow.document.getElementById("travischatroot");
                const floatButton = rootIframeEle.querySelector("div>button")
                if(floatButton){
                    floatButton.click();
                    setTimeout(() => {
                        loadingEle.remove();
                        const closeButton = rootIframeEle.querySelector("#scrollConatiner>div>button")
                        if(closeButton){
                            closeButton.onclick = function(e){
                                e.stopPropagation()
                                rootWindow.ReactNativeWebView.postMessage('close'); 
                            }
                        }
                    }, 500);
                }else{
                    setTimeout(function(){
                        checkLoad(iframe);
                    },500)
                }
            }

            const iframeEle = document.querySelector('iframe#travischatwidget')
            if(iframeEle){
                iframeEle.addEventListener('load',function(){
                    checkLoad(iframeEle)
                })
            }else{
                setTimeout(function(){
                    start()
                },500)
            }
        }
        
        window.onload = function(){
            start();
        }
    </script> 
</html>

rxjs实现贪吃蛇小游戏


description: 本文主要介绍使用rxjs和react实现一个经典的贪吃蛇游戏。
keywords:

  • rxjs
  • 游戏
  • 贪吃蛇

初识rxjs感觉惊奇,感觉很多复杂的逻辑好像用rxjs来写变得简单明了。断断续续得学习了一阵子,感觉很好,却一直没有真正在项目中使用过。学习而不使用过不了多久就会忘记,所以这次写一个小demo来加深一下印象。那么写什么好呢,想到以前用react写过贪吃蛇小游戏,据说rxjs很适合用来开发游戏类程序,所以决定用rxjs来写一个经典的贪吃蛇小游戏。

本文并不介绍rxjs原理和各种操作符的使用,主要解释一下实现的逻辑。
源码
demo展示
推荐几个我经常学习的网站:

  • 30 天精通 RxJS (00): 關於本系列文章 **同胞早年写的教程,写的很好,从原理到使用都讲的很好,还有一些生动的例子,虽然版本和写法已经有些不同,但并不影响学习。
  • 官网
  • Learn Rxjs很好的学习网站,每个操作符的解释和例子,以及各种demo。

实现UI

首先看一下整体的界面,包括Board(蛇移动的范围)、Snake(红色)、Food(绿色)、Score(分数)、暂停和重置按钮。这里我们使用react来渲染视图,当然也可以用canvas,我们只注重于逻辑,不关心ui。我们采取数据驱动视图的思维开发,逻辑和页面元素是解耦合,这种思维可以让我们很轻松从react渲染切换到canvas渲染。

image

Board

这个结构是基本不变的,只渲染一次就够了,当然把gameover移除更好,懂react的应该理解为什么用memo。

const GameOver = ({ isGameOver }) => {
    if (!isGameOver) return null;
    return (
        <h1 className="center">GAME OVER</h1>
    )
}

const Board = memo(({ winWidth, size, isGameOver }) => {
    const itemWidth = winWidth / size;

    const Row = ({ rowIndex }) => {
        return (
            <div className="rowContainer">
                {
                    Array(size).fill().map((item, index) => (
                        <span key={index} className="boardItem" style={{ width: itemWidth, height: itemWidth }}>
                            <span></span>
                        </span>
                    ))
                }
            </div>
        )
    }

    return (
        <div id="board">
            {
                Array(size).fill().map((item, index) => <Row key={index} />)
            }
            <GameOver isGameOver={isGameOver} />
        </div>
    )
})

Snake 和 Food

const Food = ({ food, itemWidth }) => {
    if (!food) return null;

    return (
        <span
            className="boardItem"
            style={{
                position: "absolute",
                width: itemWidth,
                height: itemWidth,
                left: itemWidth * food.x,
                top: itemWidth * food.y,
                backgroundColor: "green"
            }}
        >
            <span></span>
        </span>
    )

}

const Snake = memo(({ data, itemWidth }) => {
    return (
        <>
            {data.map((item, index) =>
                <span
                    // key={`${item.x}-${item.y}`}
                    key={index}
                    className="boardItem"
                    style={{
                        position: "absolute",
                        width: itemWidth,
                        height: itemWidth,
                        left: itemWidth * item.x,
                        top: itemWidth * item.y,
                        backgroundColor: "red"
                    }}
                >
                    <span></span>
                </span>
            )}
        </>
    )
})

App

把各个组件整合起来

const App = () => {
    const size = 20;
    const [ref, { width }] = useMeasure();

    const itemWidth = width / size;

    const initialState = {
        isGameOver: false,
        score: 0,
        snake: [

        ],
        food: null,
        isPaused: true
    }

    const [state, setState] = useState(initialState);

    const renderGame = ([snake, isPaused, food, score, isGameOver]) => {
        setState(state => {
            return {
                ...state,
                snake,
                isPaused,
                food,
                score,
                isGameOver
            }
        })
    }

    return (
        <>
            <Head>
                <link rel="stylesheet" href="/styles/greedySnake.css" />
            </Head>
            {width > 0 ? null : <Loading />}
            <div className="snakeGame" ref={ref} style={{ visibility: width > 0 ? "visible" : "hidden" }}>
                <Board winWidth={width} size={size} isGameOver={state.isGameOver} />
                <Snake itemWidth={itemWidth} data={state.snake} />
                <Food itemWidth={itemWidth} food={state.food} />
                <div>
                    <button id="pauseORresume">{
                        state.isPaused ? "START" : "PAUSE"
                    }</button>
                </div>
                <div>
                    <button id="reset">reset</button>
                </div>
                <span>{state.score}</span>
            </div>
        </>
    )
}

现在我们已经实现了基本的页面,接下来我们要做的写游戏的逻辑驱动各元素动起来。

实现游戏的逻辑

逻辑总体分析

首先我们分析renderGame方法,可以看到我们需要snake、food、isPaused、isGameover、score5个值来描述游戏的当前状态。那么我们设立5个流snake$、food$、isPaused$、isGameover$、score$来描述这5个状态,这5个流任何一个更新我们就重刷游戏的状态。那么现在我们的代码看起来应该是这样的。这里我们使用combineLast来获取每个流的最新值,不懂combineLast的可以先去官网看看教程。rxjs的操作符确实博大精深,一个操作符就能实现一个功能,组合起来更加精妙。

const snake$ = of([{x:1,y:1}])
const food$ = of([{x:4,y:4}])
const isPaused$ = of(true)
const isGameOver$ = of(falsse)
const score$ = of(0)

const game$ = combineLatest([snake$, pause$, food$, score$, gameOver$]).pipe(
            takeWhile(([snake, isPaused, food, score, isGameOver]) => !isGameOver, true)
        )

useEffect(()=>{
    const sub = game$.subscribe(game => renderGame(game))
    return () => {
        sub.unsubcribe();
    }
})

实现snake$

  1. 让蛇动起来
    首先我们让蛇动起来,很容易就想到interval用每隔一定时间发送一个值然后更新snake$,这里通过一个scan操作符记录上次的结果。scan和reduce很像,只是它处理的不是数组而是一个流。现在每秒发送一个值,然后snake更新x坐标加1,y坐标不变,相当于蛇向右边移动。
    const snake$ = interval(1000).pipe(
        scan((snake,_) => {
            //现在我们假设🐍的方向是向右
            let head = snake[0];
            let _x,_y;
            _x = head.x +1;
            _y = head.y;
            return [{x:_x,y:_y}]
        },[{x:1,y:1}])
    )
  1. 控制蛇的方向
    现在蛇可以动了,我们要控制它的方向,通过监听键盘上下左右键,控制蛇的下一个位置.我们加入一个新的流dir$,并更新snake$。再次强调我们不解释各种操作符的用法,请看注释或者自行学习。
    const KEY_EVENTS_DIR = [
        "ArrowUp",
        "ArrowDown",
        "ArrowLeft",
        "ArrowRight"
    ]
    const KEY_OPPOSITE = {
        ArrowUp: "ArrowDown",
        ArrowDown: "ArrowUp",
        ArrowRight: "ArrowLeft",
        ArrowLeft: "ArrowRight",
    }
    const dir$ = fromEvent(document, "keydown").pipe(
        pluck("key"), //从event中取出key
        filter((key) => KEY_EVENTS_DIR.includes(key)), //过滤掉不是方向键的event
        startWith("ArrowRight"), //设置初始方向
        distinctUntilChanged(),  //同一个键盘连续2次不会重复发送
    );
    const snake$ = interval(1000).pipe(
        withLastFrom(dir$),  //每次interval都去取dir的最新值 
        scan((snake,[_,dir]) => {
            let head = snake[0];
            let _x,_y;
            switch (dir) {
                case "ArrowRight":
                    _x = _x >= size - 1 ? 0 : _x + 1;
                    break;
                case "ArrowLeft":
                    _x = _x <= 0 ? size - 1 : _x - 1;
                    break;
                case "ArrowUp":
                    _y = _y <= 0 ? size - 1 : _y - 1;
                    break;
                case "ArrowDown":
                    _y = _y >= size - 1 ? 0 : _y + 1;
                    break;
            }
            return [{x:_x,y:_y}]
        },[{x:1,y:1}])
    )

现在我们可以通过鼠标控制蛇上下左右自由的移动了。但是我希望在手机上也能玩,可以通过监听touch事件实现。所以我们再加一个手势判断的流gestureDir$。读者应该可以看出现在我们代码逻辑很清晰,一个个简单的功能组成最终的功能,各自完成自己的工作。这就是rxjs的强大之处。想一下我们现在不用rxjs,要实现一个dir$的功能,感觉有一大堆代码要写,而且很散乱,没有一定的功力很难写的很漂亮逻辑这么清晰。

     const keyDir$ = fromEvent(document, "keydown").pipe(
         pluck("key"),
         filter((key) => KEY_EVENTS_DIR.includes(key)),
         startWith("ArrowRight"),
         distinctUntilChanged(),
     );

    const gestureDir$ = function () {
        if ('ontouchstart' in document.documentElement) {
            return fromEvent(document, "touchstart").pipe(
                switchMap((startEvent) =>
                    fromEvent(document, "touchmove").pipe(
                        takeUntil(fromEvent(document, "touchend")),
                        takeLast(1),
                        map((event) => {
                            let deltaX = event.touches[0].pageX - startEvent.touches[0].pageX;
                            let deltaY = event.touches[0].pageY - startEvent.touches[0].pageY;
                            if (deltaX > 0 && Math.abs(deltaX) > Math.abs(deltaY)) {
                                return KEY_EVENTS_DIR[3];
                            } else if (deltaX < 0 && Math.abs(deltaX) > Math.abs(deltaY)) {
                                return KEY_EVENTS_DIR[2];
                            } else if (deltaY > 0 && Math.abs(deltaY) > Math.abs(deltaX)) {
                                return KEY_EVENTS_DIR[1];
                            } else {
                                return KEY_EVENTS_DIR[0]
                            }
                        }),
                    )
                )
            )
        }
        return NEVER
    }()

    const dir$ = merge(keyDir$, gestureDir$);
    
  1. 让蛇吃苹果变长
    现在蛇可以控制方向4处移动了,但是还不能变长,我们要实现在蛇吃到苹果时长度增加1并且苹果的位置改变。这里有点复杂,因为snake$和food$相互关联了起来。snake$需要拿到当前food$的值来判断是否吃到🍎而food$也需要拿到snake的值来生成新的位置但不要和snake的位置重叠。我们引入一个新的流eatFood$来通知food$。
     //随机生成食物
    const createFood = (size, data) => {
         let x = Math.floor(Math.random() * size);
         let y = Math.floor(Math.random() * size);
         if (data.some(item => item.x === x && item.y === y)) {
             return createFood(size, data);
         }
         return { x, y }
     }
    
     const eatFood$ = new BehaviorSubject([]);
    
     const food$ = eatFood$.pipe(
         map(snake => createFood(size, snake)),
         shareReplay(1), //为什么要用shareReplay?
     )
    
     const snake$ = interval(1000).pipe(
         withLastFrom(dir$,food$),  //每次interval都去取dir的最新值 
         scan((snake,[_,dir,food]) => {
             let head = snake[0];
             let _x,_y;
             switch (dir) {
                 case "ArrowRight":
                     _x = _x >= size - 1 ? 0 : _x + 1;
                     break;
                 case "ArrowLeft":
                     _x = _x <= 0 ? size - 1 : _x - 1;
                     break;
                 case "ArrowUp":
                     _y = _y <= 0 ? size - 1 : _y - 1;
                     break;
                 case "ArrowDown":
                     _y = _y >= size - 1 ? 0 : _y + 1;
                     break;
             }
             snake.unshift({ x: _x, y: _y });
                 //吃到🍎,通知更新food$
             if (food.x === _x && food.y === _y) {
                 eatFood$.next(snake) //通知food$更新
             } else {
                 snake.pop();
             }
             return [...snake];
         },[{x:1,y:1}])
     )
  2. 增加暂停功能
    现在蛇已经能自由移动,通过吃🍎不断的变长了,游戏的基本逻辑已经实现。接下来我们增加一个暂停和恢复的功能。请注意看pause实现,我们通过switchMap操作符来控制是否往下面的流发送值。
    const pauseClick$ = fromEvent(document.getElementById("pauseORresume"), "click");
    
    const pauseKey$ = fromEvent(document, "keydown").pipe(
        pluck("code"),
        filter((code) => code === "Space")
    )
    
    const pause$ = merge(pauseClick$, pauseKey$).pipe(
        startWith(true),
        scan((current, prev) => current ? false : true, false)
    )
    
    const interval$ = interval(200);
    
    const snake$ = pause$.pipe(
        switchMap((isPaused) => isPaused ? NEVER : inteval$),
        startWith("init"),
        withLastFrom(dir$,food$),  //每次interval都去取dir的最新值 
        scan((snake,[_,dir,food]) => {
            let head = snake[0];
            let _x,_y;
            switch (dir) {
                case "ArrowRight":
                    _x = _x >= size - 1 ? 0 : _x + 1;
                    break;
                case "ArrowLeft":
                    _x = _x <= 0 ? size - 1 : _x - 1;
                    break;
                case "ArrowUp":
                    _y = _y <= 0 ? size - 1 : _y - 1;
                    break;
                case "ArrowDown":
                    _y = _y >= size - 1 ? 0 : _y + 1;
                    break;
            }
            snake.unshift({ x: _x, y: _y });
                //吃到🍎,通知更新food$
            if (food.x === _x && food.y === _y) {
                eatFood$.next(snake) //通知food$更新
            } else {
                snake.pop();
            }
            return [...snake];
        },[{x:1,y:1}])
    )
  3. 增加gameover判断
    现在游戏逻辑基本完成,但是什么时候游戏结束呢,我们还需要实现isGameOver$的逻辑,当蛇咬到自己的时候游戏结束,在snake$里做判断就行。
    const checkGameOver = (snake) => {
        let head = snake[0];
        return snake.slice(1).some(item => item.x === head.x && item.y ===  head.y);
    }
    const snake$ = pause$.pipe(
        switchMap((isPaused) => isPaused ? NEVER : inteval$),
        startWith("init"),
        withLastFrom(dir$,food$),  //每次interval都去取dir的最新值 
        scan((snake,[_,dir,food]) => {
            let head = snake[0];
            let _x,_y;
            switch (dir) {
                case "ArrowRight":
                    _x = _x >= size - 1 ? 0 : _x + 1;
                    break;
                case "ArrowLeft":
                    _x = _x <= 0 ? size - 1 : _x - 1;
                    break;
                case "ArrowUp":
                    _y = _y <= 0 ? size - 1 : _y - 1;
                    break;
                case "ArrowDown":
                    _y = _y >= size - 1 ? 0 : _y + 1;
                    break;
            }
            snake.unshift({ x: _x, y: _y });
                //吃到🍎,通知更新food$
            if (food.x === _x && food.y === _y) {
                eatFood$.next(snake) //通知food$更新
            } else {
                snake.pop();
            }
            if (checkGameOver(snake)) {
                gameOver$.next(true)
            }
            return [...snake];
        },[{x:1,y:1}])
    )
  1. 增加reset功能
    游戏结束时我们还需要重置游戏的功能。我们先把之前的逻辑整理成一个方法createGame,然后再增加一个startGame的方法,每次reset就重建一个game$达到reset的作用。最终代码如下:
        const createGame = () => {
        const gameOver$ = new BehaviorSubject(false);

        const keyDir$ = fromEvent(document, "keydown").pipe(
            pluck("key"),
            filter((key) => KEY_EVENTS_DIR.includes(key)),
            startWith("ArrowRight"),
            distinctUntilChanged(),
        );

        const gestureDir$ = function () {
            if ('ontouchstart' in document.documentElement) {
                return fromEvent(document, "touchstart").pipe(
                    switchMap((startEvent) =>
                        fromEvent(document, "touchmove").pipe(
                            takeUntil(fromEvent(document, "touchend")),
                            takeLast(1),
                            map((event) => {
                                let deltaX = event.touches[0].pageX - startEvent.touches[0].pageX;
                                let deltaY = event.touches[0].pageY - startEvent.touches[0].pageY;
                                if (deltaX > 0 && Math.abs(deltaX) > Math.abs(deltaY)) {
                                    return KEY_EVENTS_DIR[3];
                                } else if (deltaX < 0 && Math.abs(deltaX) > Math.abs(deltaY)) {
                                    return KEY_EVENTS_DIR[2];
                                } else if (deltaY > 0 && Math.abs(deltaY) > Math.abs(deltaX)) {
                                    return KEY_EVENTS_DIR[1];
                                } else {
                                    return KEY_EVENTS_DIR[0]
                                }
                            }),
                        )
                    )
                )
            }
            return NEVER
        }()


        const dir$ = merge(keyDir$, gestureDir$);

        const pauseClick$ = fromEvent(document.getElementById("pauseORresume"), "click");
        const pauseKey$ = fromEvent(document, "keydown").pipe(
            pluck("code"),
            filter((code) => code === "Space")
        )
        const pause$ = merge(pauseClick$, pauseKey$).pipe(
            startWith(true),
            scan((current, prev) => current ? false : true, false)
        )

        const eatFood$ = new BehaviorSubject([]);

        const score$ = eatFood$.pipe(
            scan((score, _) => {
                return score + 1
            }, -1)
        )

        const food$ = eatFood$.pipe(
            map(snake => createFood(size, snake)),
            shareReplay(1),
        )

        //增加小需求每吃5个苹果速度增加
        const inteval$ = score$.pipe(
            filter(score => score % 5 === 0),
            map(score => {
                let level = Math.floor(score / 5);
                level = level >= 3 ? 3 : level;
                return INTERVAL_TIMES[level];
            }),
            distinctUntilChanged(),
            switchMap((time) => interval(time)),
        )

        const snake$ = pause$.pipe(
            switchMap((isPaused) => isPaused ? NEVER : inteval$),
            startWith("init"),
            withLatestFrom(dir$, food$),
            //快速切换相反方向导致蛇吃到自己
            scan((prev, [_, dir, food]) => {
                if (KEY_OPPOSITE[dir] === prev[0]) {
                    return [prev[0], food]
                }
                return [dir, food]
            }, []),
            scan((snake, [dir, food]) => {
                let head = snake[0];
                let _x = head.x;
                let _y = head.y;
                switch (dir) {
                    case "ArrowRight":
                        _x = _x >= size - 1 ? 0 : _x + 1;
                        break;
                    case "ArrowLeft":
                        _x = _x <= 0 ? size - 1 : _x - 1;
                        break;
                    case "ArrowUp":
                        _y = _y <= 0 ? size - 1 : _y - 1;
                        break;
                    case "ArrowDown":
                        _y = _y >= size - 1 ? 0 : _y + 1;
                        break;
                }
                snake.unshift({ x: _x, y: _y });
                //吃到🍎,通知更新food$
                if (food.x === _x && food.y === _y) {
                    eatFood$.next(snake)
                } else {
                    snake.pop();
                }
                if (checkGameOver(snake)) {
                    gameOver$.next(true)
                }
                return [...snake];
            }, [{ x: 1, y: 1 }]),
        );

        const game$ = combineLatest([snake$, pause$, food$, score$, gameOver$]).pipe(
            takeWhile(([snake, isPaused, food, score, isGameOver]) => !isGameOver, true)
        )
        return game$;
    }


    const renderGame = ([snake, isPaused, food, score, isGameOver]) => {
        setState(state => {
            return {
                ...state,
                snake,
                isPaused,
                food,
                score,
                isGameOver
            }
        })
    }

    const startGame = () => {
        const reset$ = fromEvent(document.getElementById("reset"), "click");
        const game$ = merge(of("startGame"), reset$).pipe(
            switchMap(x => createGame()),
        )
        const sub = game$.subscribe(game => renderGame(game))
        return sub;
    }

    useEffect(() => {
        let sub = startGame();

        return () => {
            sub.unsubscribe();
        }
    }, [])

总结

通过写一个贪吃蛇游戏确实加深了我对rxjs的理解,之前我也以为我差不多懂rxjs的用法了。但是真正写的时候为了实现各种需求还是遇到了很多问题。以我写这个demo的经验来看,rxjs真的没有说的那么简单,为了实现不同功能在不同的流之间穿插很考验你对rxjs真正理解的程度。但是不得不承认rxjs真的强大,通过组合各种简单方法实现一个复杂的功能,代码思路清晰逻辑分明,容易扩展和维护。那么到底要不要用rxjs呢?我的观点是一定要比较深入理解rxjs了再用,不然你会遇到很多麻烦的。rxjs可能不像react一样简单,随便看看文档懂了jsx和生命周期就可以动手写了。

script标签上async 和 defer的区别

经常看到一些cdn的script标签上有async和defer,知道大概是异步加载却一直没深究到底有什么用,今天看到一片博文,大概了解了这两者的作用和区别。

JS 加载阻塞

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <script src="https://cdn.bootcss.com/jquery/2.1.4/jquery.min.js"></script>
</head>
<body>
    <h1>我是 h1 标签</h1>
</body>
</html>

将浏览器网速调到50k/s会发现,刷新浏览器,观察 Elements 面,一直未加载出 h1 标签(期间页面一直白屏),直到 JS 加载完成后,DOM 中才出现,这足以说明了 JS 会阻塞定义在其之后的 DOM 的加载。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <script async src="https://cdn.bootcss.com/jquery/2.1.4/jquery.min.js"></script>
</head>
<body>
    <h1>我是 h1 标签</h1>
</body>
</html>

在script上加上async(异步)或者defer(延迟)会发现都不会阻塞DOM的加载。那么二者有什么区别呢?分析浏览遇到script的流程:

  1. 暂停解析 DOM;
  2. 执行 script 里的脚本,如果该 script 是外链,则会先下载它,下载完成后立刻执行;
  3. 执行完成后继续解析剩余 DOM。

但是defer和async有所不同。

defer的特点

  • 对于 defer 的 script,浏览器会继续解析 html,且同时并行下载脚本,等 DOM 构建完成后,才会开始执行脚本,所以它不会造成阻塞;
  • defer 脚本下载完成后,执行时间一定是 DOMContentLoaded 事件触发之前执行;
  • 多个 defer 的脚本执行顺序严格按照定义顺序进行,而不是先下载好的先执行;

async 特点

  • 对于 async 的 script,浏览器会继续解析 html,且同时并行下载脚本,一旦脚本下载完成会立刻执行;和 defer 一样,它在下载的时候也不会造成阻塞,但是如果它下载完成后 DOM 还没解析完成,则执行脚本的时候是会阻塞解析的;
  • async 脚本的执行 和 DOMContentLoaded 的触发顺序无法明确谁先谁后,因为脚本可能在 DOM 构建完成时还没下载完,也可能早就下载好了;
  • 多个 async,按照谁先下载完成谁先执行的原则进行,所以当它们之间有顺序依赖的时候特别容易出错。

defer 和 async 都只能用于外部脚本,如果 script 没有 src 属性,则会忽略它们。

总结

根据defer和async的不同特点可以得出一些结论,在一些需要相互依赖的脚本,按顺序执行的脚本需要使用defer;而没有依赖关系谁先下载完谁先执行就好的情况下可以用async。

参考文献

  1. 探究网页资源究竟是如何阻塞浏览器加载的
  2. 浅谈script标签中的async和defer

next-blog


description: some desc
keywords:

  • 1
  • 2
  • 3

测试写一个next-blog

react-native 0.62在xcode13遇到build error解决

最近为了将app项目发布到ios15版本,升级了xcode到13,遇到几个bug,这里记录解决方案。

1. No matching function for call to 'RCTBridgeModuleNameForClass'

# 在 ios/Podfile 中加入一下代码
def find_and_replace(dir, findstr, replacestr)
    Dir[dir].each do |name|
        text = File.read(name)
        replace = text.gsub(findstr,replacestr)
        if text != replace
            puts "Fix: " + name
            File.open(name, "w") { |file| file.puts replace }
            STDOUT.flush
        end
    end
    Dir[dir + '*/'].each(&method(:find_and_replace))
  end
post_install do |installer|
  ## 以下 Fix for XCode 12.5
    find_and_replace(
    "../node_modules/react-native/React/CxxBridge/RCTCxxBridge.mm",
    "_initializeModules:(NSArray<id<RCTBridgeModule>> *)modules", 
    "_initializeModules:(NSArray<Class> *)modules")
    
    find_and_replace(
    "../node_modules/react-native/ReactCommon/turbomodule/core/platform/ios/RCTTurboModuleManager.mm",
    "RCTBridgeModuleNameForClass(strongModule))", 
    "RCTBridgeModuleNameForClass(Class(strongModule)))"
    )
    installer.pods_project.targets.each do |target|
      target.build_configurations.each do |config|
        config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '9.0'
      end
    end
  end

2. undefined symbol: _swift_force_load$_swiftfileprovider

rn官方最新解决方案

3. 删除flipper相关代码

相关链接

react-native下载pdf文件

很久就实现过的功能,今天又遇到了,竟然无从下手,只好去翻翻旧代码并在这里记录一下也方便日后查看。

需求分析

  • ajax请求pdf文件
  • 转成base64
  • 下载到手机,需要能从外部App打开

1. 首先获取pdf文件二进制

async function getPdfBinary(url) {
    return new Promise((resolve, reject) => {
        var xhr = new XMLHttpRequest();
        xhr.open("GET", url, true);
        xhr.responseType = "arraybuffer"; // get the binary 
        xhr.setRequestHeader('content-type', 'application/json');
        xhr.onload = function (event) {
            var arrayBuffer = xhr.response;
            var byteArray = new Uint8Array(arrayBuffer);
            var len = byteArray.byteLength;
            var binary = ""
            for (var i = 0; i < len; i++) {
                binary += String.fromCharCode(byteArray[i]);
            }
            resolve(binary);
        }
        xhr.send();
    })
}

2. 转成base64字符串

//react-native 并没有btoa和atob方法 需要自己实现
function base64_encode(str) {
    var c1, c2, c3;
    var base64EncodeChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    var i = 0,
        len = str.length,
        string = '';

    while (i < len) {
        c1 = str.charCodeAt(i++) & 0xff;
        if (i == len) {
            string += base64EncodeChars.charAt(c1 >> 2);
            string += base64EncodeChars.charAt((c1 & 0x3) << 4);
            string += "==";
            break;
        }
        c2 = str.charCodeAt(i++);
        if (i == len) {
            string += base64EncodeChars.charAt(c1 >> 2);
            string += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4));
            string += base64EncodeChars.charAt((c2 & 0xF) << 2);
            string += "=";
            break;
        }
        c3 = str.charCodeAt(i++);
        string += base64EncodeChars.charAt(c1 >> 2);
        string += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4));
        string += base64EncodeChars.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6));
        string += base64EncodeChars.charAt(c3 & 0x3F)
    }
    return string
}

3. 下载到android手机

今天刚好在stackoverflow上回答别人的问题,他需要的是expo,所以写了个expo版本的。Base64 String to pdf JavaScript React Native then download
For expo

const downloadForAos = async (pdfBase64Str) => {
    const folder = FileSystem.StorageAccessFramework.getUriForDirectoryInRoot("test");
    const permissions = await FileSystem.StorageAccessFramework.requestDirectoryPermissionsAsync(folder);
    if (!permissions.granted) return;

    let filePath = await FileSystem.StorageAccessFramework.createFileAsync(permissions.directoryUri, "test.pdf", "application/pdf");
    // let filePath = "content://com.android.externalstorage.documents/tree/primary%3Atest/document/primary%3Atest%2Ftest.txt";
    console.log(pdfBase64Str, "====");
    try {
        await FileSystem.StorageAccessFramework.writeAsStringAsync(filePath, pdfBase64Str, { encoding: FileSystem.EncodingType.Base64 });
        alert("download success!")
    } catch (err) {
        console.log(err);
    }
}

For android

const downloadForAos = async (pdfBase64Str) => {
    const fileName = `/${Date.now()}_report.pdf`;
    const path = RNFS.DownloadDirectoryPath + fileName;
    try {
        const granted = await PermissionsAndroid.request(
            PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
        );
        if (granted) {
            RNFS.writeFile(path, pdfBase64Str, "base64").then(res => {
                ToastAndroid.show(I18n.t("myHealth.Downloaded"), ToastAndroid.SHORT);
            }).catch(error => {
                ToastAndroid.show(I18n.t("myHealth.DownloadFailed"), ToastAndroid.SHORT);
            })
        }
    } catch (err) {
        console.warn(err);
    }
}

4. 下载到ios手机

ios不能直接下载文件到外部app,只能通过分享到file app实现。找了很久没找到其他办法,有其他办法的大神请多多指教。

const downloadPdfForIos = async (pdf) => {
    const url = "data:application/pdf;base64," + pdf;
    const shareOptions = {
        title: 'image report',
        failOnCancel: false,
        saveToFiles: true,
        url: url, // base64 with mimeType or path to local file
    };
    try {
        const ShareResponse = await Share.open(shareOptions);
        Alert.alert(
            null,
            I18n.t("myHealth.Downloaded"),
            [
                {
                    text: I18n.t("common.OK"),
                    onPress: () => null
                }
            ]
        );
    } catch (error) {
        if (error.error && error.error.code === "ECANCELLED500") {
            console.warn("canceled");
        } else {
            Alert.alert(
                null,
                I18n.t("myHealth.DownloadFailed"),
                [
                    {
                        text: I18n.t("common.OK"),
                        onPress: () => null
                    }
                ]
            );
        }
    }
}

使用imagemagic制作瓦片地图

之前有个项目客户给一张精度很高的图让做成那种能够缩放的地图的效果。一开始的想法是直接加载大图,然后通过手势去控制图片的scale来实现,后来觉得不妥,高精度图片的体积实在太大,加载时间太长,效果很不好。通过研究发现openlayers这种专门用来加载地图的库才是更好的选择。但是怎么把图片制作成那种瓦片地图是个问题。

瓦片地图金字塔模型是一种多分辨率层次模型,从瓦片金字塔的底层到顶层,分辨率越来越低,但表示的地理范围不变。首先确定地图服务平台所要提供的缩放级别的数量N,把缩放级别最高、地图比例尺最大的地图图片作为金字塔的底层,即第0层,并对其进行分块,从地图图片的左上角开始,从左至右、从上到下进行切割,分割成相同大小(比如256x256像素)的正方形地图瓦片,形成第0层瓦片矩阵;在第0层地图图片的基础上,按每像素分割为2×2个像素的方法生成第1层地图图片,并对其进行分块,分割成与下一层相同大小的正方形地图瓦片,形成第1层瓦片矩阵;采用同样的方法生成第2层瓦片矩阵;…;如此下去,直到第N一1层,构成整个瓦片金字塔。

原理

瓦片地图的原理很简单。假设客户给的原图为(5124,5124),这是我们地图能够达到的最高分辨率,但是假设我们的视窗只有512512,此时我们只能看到左上角1/16的部分。假设用户最开始看到的图包含整个地图,也就是512512,此时地图被缩放了16倍,宽高个4倍,此时设level为1,那么当放大一倍时,此时地图实际图片大小为(5122,5122),此时level为2,当再放大一倍时,此时level为3,刚好为原图,分辨率最高。那么我们只要想办法把原图按不同level缩放切割就行。level1就是直接宽高各缩放4倍,level2各缩放2倍,再把它切成4张,每张大小为512512,level3的时候不用缩放,直接切成16张,同样每张512512.我们把
不同level的图片放在不同的文件夹下,然后用能够加载瓦片地图的库就能加载,例如openlayers。所以我们剩下的问题就是怎么切图了。

import 'ol/ol.css';
import Map from 'ol/Map';
import TileLayer from 'ol/layer/Tile';
import View from 'ol/View';
import XYZ from 'ol/source/XYZ';

var map = new Map({
  target: 'map',
  layers: [
    new TileLayer({
      source: new XYZ({
        url:
          'https://localhost:9080/tiles/{z}-{x}-{y}.png'    //因为我的图片是 1-1-1这种形式组成所以这里是这样,如果你的图片是按等级放在不同文件夹要写成 https://localhost:9080/tiles/{z}/{x}/{y}.png 具体怎么放要看你如何组织的tiles结构
      }),
    }) ],
  view: new View({
    center: [-472202, 7530279],
    zoom: 12,
  }),
});

QQ20210615-225842@2x

使用imagemagic处理图片

Use ImageMagick® to create, edit, compose, or convert digital images. It can read and write images in a variety of formats (over 200) including PNG, JPEG, GIF, WebP, HEIC, SVG, PDF, DPX, EXR and TIFF. ImageMagick can resize, flip, mirror, rotate, distort, shear and transform images, adjust image colors, apply various special effects, or draw text, lines, polygons, ellipses and Bézier curves.

imagemagic是一个强大的图片处理工具,可以用命令行直接处理图片,可以很方便的实现各种图片处理操作。虽然很强大但不得不说文档很晦涩难懂,我也只是为了完成这个任务熟悉了几个命令而已,完全谈不上精通。
[我们的切图工具分2个步骤,第一步就是按不同level切图,但是因为并不是刚好如原理分析里讲的那么刚好就是整数倍,地图原图往往不是刚好是正方形,比如切的瓦片每个为512512,切到边上的时候可能只有360400这样的,这时需要把它补成512*512,不然会有奇怪的现象发生。注意,脚本里有些参数要根据你的实际情况自己设置。源码地址

#! /bin/bash
#cropFile.sh
INDEX=1
IMAGE_WIDTH=10752   #原图宽度
IMAGE_HEIGHT=21504  #原图高度

for file in `ls "originImages"`;do #将原图放在orginalImages文件夹下
	echo "processing ${file}"
	file_base=$(basename $file .png) #拿到原图名称
	tiles_dir="${file_base}_tiles"   #碰到有无目标文件夹,有则直接下一步,没有就建立文件夹
	if [ -e "${tiles_dir}" ] && [ -d "${tiles_dir}" ];then 
		echo "${tiles_dir} exisit"
	else
		echo "mkdir ${tiles_dir}"
		mkdir "${tiles_dir}"
		echo "mkdir ${tiles_dir} completed"
	fi
	for (( i=1; i<=7; i++));do    #根据你自己的需求调节所需的等级
		echo "processing ${file} scale ${i}"
		((level= 7 - i))
		((scale= 2 ** (i-1)))
		((width= $IMAGE_WIDTH / $scale))
		((height= $IMAGE_HEIGHT / $scale))
		resizeFormat="${width}x${height}"
		echo $resizeFormat
		convert "originImages/${file}" -resize $resizeFormat -crop 512x512 -set filename:tile ./${tiles_dir}/${level}-%[fx:page.x/512]-%[fx:page.y/512] %[filename:tile].png
		echo "complete ${file} scale ${i}"
	done
	echo "complete ${file}"
	let INDEX=${INDEX}+1
done
#! /bin/bash
#fillSize.sh
START=`date +%s%N`;
for folder in `ls | grep "H*tiles"`;do  #找到所有之前切片生成的文件夹,遍历其中找到其中尺寸不对的,将其放置在512*512透明图的左上角
	echo $folder
	for image in `ls $folder`;do
		echo $image
		ratio=`identify "${folder}/${image}" | cut -d " " -f 3`
		if [ "$ratio" != "128x128" ];then
			echo $ratio
			convert "${folder}/${image}" -background transparent -gravity NorthWest -extent 512x512 "${folder}/${image}"
		fi 
	done
done
END=`date +%s%N`;
time=$((END-START))
time=`expr $time / 1000000`
echo "totally spend time ${time}"

webpack分包学习笔记

1. optimization.runtimeChunk

设置runtimeChunk是将包含chunks 映射关系的 list单独从 app.js里提取出来,因为每一个 chunk 的 id 基本都是基于内容 hash 出来的,所以每次改动都会影响它,如果不将它提取出来的话,等于app.js每次都会改变。缓存就失效了。设置runtimeChunk之后,webpack就会生成一个个runtime~xxx.js的文件。

module.exports = {
  //...
  optimization: {
    runtimeChunk: {
      name: (entrypoint) => `runtime~${entrypoint.name}`,
    },
    // boolean
    // string single | multiple
    // object name:()=>null
};

2. optimization.moduleIds 和 optimization.chunkIds

webpack稳定moduleid和chunkid以实现持久化缓存的梳理

3. optimization.splitChunks

理解webpack4.splitChunks

理解animation-fill-mode属性


description: 理解css动画属性animation-fill-mode为both的情况
keywords:

  • css
  • animation
  • animation-fill-mode

animation-fill-mode作为css3动画的一个重要属性很常见,但是大多用于把动画停在最后一帧,也就是forwards值的用法。在多年的前端开发中很少关注其他几个值,今天看文档发现对于backwards(一直以为就是回到初始状态和none没什么两样)和both一直没有正确的了解,文档英文描述晦涩难懂,demo也没有什么详细说明,所以打算一探究竟。

属性值

  • none 默认值

The animation will not apply any styles to the target when it's not executing. The element will instead be displayed using any other CSS rules applied to it. This is the default value.

在动画不执行时,不会赋予任何target任何style,动画执行结束回归原样
  • forwards

The target will retain the computed values set by the last keyframe encountered during execution.

target在动画执行结束后保持最后一帧的style
  • backwards

The animation will apply the values defined in the first relevant keyframe as soon as it is applied to the target, and retain this during the animation-delay period.

动画将在执行的第一时间将style赋予target,包括delay的时候,这就是none和backwards的区别,当第一帧的属性和target原来的属性不同的时候特别明显。
  • both

The animation will follow the rules for both forwards and backwards, thus extending the animation properties in both directions.
动画将同时有forwards和afterwards的特性。

demo

比较4个值不同的表现

  • none 小球先是绿色,delay3秒后才变为红色,最后变黄色,回到原点
  • forwards 小球先是绿色,delay3秒后才变为红色,最后变黄色,保持在终点
  • afterwards 小球直接变红色,delay3秒后,动画开始,慢慢变黄色,动画结束回到原点
  • both 小球直接变红色,delay3秒后,动画开始,慢慢变黄色,动画结束保持在终点

总结

以前想当然的以为afterwards就是回到原点,forwards就是保持在终点,其实是错误的。afterwards是在动画开始时第一时间选择第一帧的style,有delay就是delay开始时。

No signature found in package of version 2

最近googleplay要求将android target version升级至30,不然不能上store。升级后又发现在android11上install失败。查找原因发现,在安卓11的机器上adb install后出现这个错误,search后发现原来android11以后不再支持v1签名,必须使用v2签名。

image

v1签名和v2签名的区别

V1:可对签名后的文件,作适当修改,并重新压缩。 
V2:不能对签名后的 APK作任何修改,包括重新解压。因为它是针对字节进行的签名,所以任何改动都会影响最终结果。

解决方案

  1. 手动
signingConfigs {
        debug {
            storeFile file('xxxxx.jks')
            storePassword '123456'
            keyAlias 'dev'
            keyPassword '123456'
            v2SigningEnabled true
        }
    }
  1. appcenter
    删掉keystore文件,重新上传就自动好了
    https://github.com/microsoft/appcenter/issues/1990#issuecomment-856960698

使用javascript生成pdf文件

一个教育网站项目有个需求,需要在学员完成某些测试后生成证书并下载。本以为应该是调用后端的接口生成,但是后端也说不太懂,只好前端来研究一下解决方案。google一下后发现比较靠谱的有2种方案。

  • 使用jspdf和html2canvas这2个库在前端实现
  • 使用nodejs通过puppeteer 在后端生成

下面分别说说这2种方法的实现。

使用jspdf生成pdf

首先编写pdf模版的html(我这里是用react写的)。

<Cert
    chineseName={chineseName}
    englishName={englishName}
    unitInfo={unitInfo}
/>

因为这个模版只是用来生成pdf,并不用显示,所以通过css将其放在视野之外。

.cert {
    width: 870px;
    height: 615px;
    position: absolute;
    // background:transparent url($imgData) ;
    // background-size: 100%;
    // top:-30px;
    left: 5000px;
    visibility: hidden;
    z-index: 99;
}

核心代码,先用html2canvas将html转成canvas,再将canvas转成image dataurl,再将image写入pdf中。

const savePDF = async () => {
    setIsLoading(true);
    try {
        const jsPDF = (await import("jspdf")).default; //引入jspdf
        const html2canvas = (await import("html2canvas")).default; //引入html2canvas
        let canvas = await html2canvas(document.querySelector("#cert"), {
            scrollY: 0,  //不设置此属性 滚动视图时会导致下载的pdf有部分不显示
            onclone: (clonedDoc) => {
                clonedDoc.getElementById("cert").style.visibility = 'visible'; //将pdf模版设为可见
            }
        })
        var imgData = canvas.toDataURL('image/png');
        var doc = new jsPDF('l', 'mm', "a4", true); 
        var width = doc.internal.pageSize.getWidth();
        var height = doc.internal.pageSize.getHeight();
        doc.addImage(imgData, 'PNG', 0, 0, width, height, "", "FAST"); //FAST 是否压缩
        // let pdf = doc.output('blob');
        // let form = new FormData();
        doc.output('save', 'Certificate.pdf');
    } catch (err) {
        console.log(err);
    } finally {
        setIsLoading(false)
    }
}

这就大功告成了?高兴的太早!😭😭😭问题总是层出不穷的多。在用此方法中一共遇到三个较大的问题。

  1. 初步实现功能提交后,一天qa突然和我说怎么生成的pdf上半部分是空白的。没办法,找找找。通过google,发现是html2canvas时会根据用户滚动的位置来计算的,设置scrollY: 0解决。
  2. 之前在dev场中测试并没有发现问题,但是上了uat后发现pdf中图片没加载。最后发现是因为uat场图片全部放在了cdn的缘故,pdf不会加载非同源站的图片也就跨域,设置useCORS也没有用。最后直接把pdf中用到的图片换成base64解决。
  3. 不同的浏览器和设备生成的pdf文件size不同。有的只有几M,有的十几M。这个应该和设备的分辨率有关。设置了是否压缩,可以调节pdf的size,但是还是无法统一生成的pdf的size。

使用puppeteer后端生成pdf

做过爬虫的应该都听过鼎鼎大名的phantomjs,当年这个库很火,可以说是一些比较复杂网站的爬虫最终解决方案。因为它可以让你用js操作一个真正的浏览器去访问网站。puppeteer是google推出的headless chrome,可以说是phantomjs的替代品,自从这个库出世,phantomjs好像就停止维护了。

Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol. Puppeteer runs headless by default, but can be configured to run full (non-headless) Chrome or Chromium.

为了解决上面说的不同设备生成的pdf size不同的问题,我们可以使用puppeteer来在后端生成pdf。
因为生成的证书如姓名成绩之类的是动态的,所以要用模版动态生成,因为我的项目是react,所以直接用jsx生成模板,用rendertostring生成html字符串。也有一些style和图片处理比较麻烦,可以看我代码,但是这里只是简单实现。

const PdfTemplate = ({ imgSrc }) => {
    return (
        <div className="bg">
            <h2>heading</h2>
            <div>
                content
            </div>
            <img src={imgSrc} alt="" style={{ width: "100%", height: 300 }} />
        </div>
    )
}

const createHtmlStr = () => {
    const imagePath = __dirname + "/../../../../public/images/bg.jpeg";
    const imgData = fs.readFileSync(imagePath).toString("base64");
    const imgSrc = `data:image/jpeg;base64,${imgData}`;
    const content = ReactDOMServer.renderToString(<PdfTemplate imgSrc={imgSrc} />)
    return `
        <html>
            <head>
                <style>
                    html,body{
                        margin:0;
                        padding:0;
                        height:100%
                    }
                    .bg{
                        height:100%;
                        position:relative;
                    }
                </style>
            </head>
            <body>
                ${content}
            </body>
        </html>
    `
}

后端代码

export default async function handler(req, res) {
    console.log("start")
    const browser = await puppeteer.launch({
        args: ['--disable-dev-shm-usage', '--no-sandbox']
    });
    const page = await browser.newPage();
    ;
    const html = createHtmlStr();
    console.log(html, "====")
    // await page.goto("http://localhost:3000/");
    await page.setContent(html);
    const pdf = await page.pdf({
        format: 'A4',
        printBackground: true,
        '-webkit-print-color-adjust': 'exact',
    });
    await browser.close();
    console.log("end render");
    res.setHeader("Content-Type", 'application/pdf');
    res.status(200);
    res.send(pdf);
}

前端代码

  const downloadPDF = async () => {
          let response = await fetch("/api/createPdf", {
              responseType: 'arraybuffer',
              headers: {
                  'Accept': 'application/pdf'
              }
          });
          let blob = await response.blob();
          // const blob = new Blob([response.body], { type: 'application/pdf' })
          const link = document.createElement('a')
          link.href = window.URL.createObjectURL(blob)
          link.download = `your-file-name.pdf`
          link.click()
  }

总结

  1. 在前端通过截屏html元素生成pdf,简单方便,不需要后端配合,直接在前端生成pdf,缺点就是生成的pdf文件大小不统一。
  2. 在后端用puppeteer生成pdf,可以解决上述方法的问题,但是要引入一个新的接口,需要加一个node服务,这个项目因为我们是用nextjs写的,所以可以很方便的加一个api接口。但是一般我们的后端都是java写的,要额外的开一个node服务不是特别方便。

"Android build error AAPT: error: resource android:attr/lStar not found"

问题

Execution failed for task ':app:processDebugResources'.

A failure occurred while executing com.android.build.gradle.internal.tasks.Workers$ActionFacade
Android resource linking failed
/Users/devingong/.gradle/caches/transforms-2/files-2.1/f616efa5326a198920e8ac1c64cb6655/core-1.8.0-alpha02/res/values/values.xml:105:5-114:25: AAPT: error: resource android:attr/lStar not found.

解决办法

添加androidXCore = "1.6.0"

buildscript {
    ext {
        buildToolsVersion = "28.0.3"
        minSdkVersion = 21
        compileSdkVersion = 29
        targetSdkVersion = 28
        adroidXCore = "1.6.0"
    }
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath("com.android.tools.build:gradle:3.5.3")
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.