feat: support canvans 2d for cache (#6367)

* feat: support canvans 2d for cache

* fix: image src hydrate

* chore: update lock

* fix: img not match when ssr

* fix: canvas dispalay

* chore: remove runtime

* chore: add event for taobao

* chore: add window.WindVane

* chore: add declare

* chore: add changeset

* feat: add cacheCanvasToStorage to ref

* chore: modify function

* feat: add @ice/cache-canvas

* feat: replace mounted

* chore: add dependence to useCallback

* chore: add try to Storage

* chore: import CacheCanvas from ice in example

* chore: add changeset

* chore: add conment

* feat: add rendered

* feat: add getSnapshot

* feat: modify

* chore: add import meta

* chore: add CacheCanvasProps
This commit is contained in:
染陌同学 2023-07-17 14:07:27 +08:00 committed by GitHub
parent cf8a78e379
commit 018238f904
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 600 additions and 7 deletions

View File

@ -0,0 +1,5 @@
---
'@ice/cache-canvas': patch
---
feat: support cache of 2d cavans

View File

@ -0,0 +1,5 @@
---
'@ice/plugin-canvas': patch
---
feat: support cache of 2d cavans

View File

@ -0,0 +1,2 @@
defaults
ios_saf 9

View File

@ -0,0 +1,10 @@
import { defineConfig } from '@ice/app';
import canvasPlugin from '@ice/plugin-canvas';
export default defineConfig(() => ({
plugins: [
canvasPlugin(),
],
ssr: true,
ssg: false,
}));

View File

@ -0,0 +1,25 @@
{
"name": "@examples/canvas-project",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "ice start",
"build": "ice build"
},
"description": "",
"author": "",
"license": "MIT",
"dependencies": {
"@ice/app": "workspace:*",
"@ice/plugin-canvas": "workspace:*",
"@ice/runtime": "workspace:*",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"@ice/cache-canvas": "workspace:*"
},
"devDependencies": {
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"webpack": "^5.86.0"
}
}

View File

@ -0,0 +1,3 @@
import { defineAppConfig } from 'ice';
export default defineAppConfig(() => ({}));

View File

@ -0,0 +1,7 @@
export default function Bar() {
return (
<div>
bar
</div>
);
}

View File

@ -0,0 +1,22 @@
import { Meta, Title, Links, Main, Scripts } from 'ice';
function Document() {
return (
<html>
<head>
<meta charSet="utf-8" />
<meta name="description" content="ICE Demo" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Title />
<Links />
</head>
<body>
<Main />
<Scripts />
</body>
</html>
);
}
export default Document;

View File

@ -0,0 +1,65 @@
import { definePageConfig, CacheCanvas } from 'ice';
import { useRef } from 'react';
import styles from './index.module.css';
export type RefCacheCanvas = {
cacheCanvasToStorage: () => void;
};
const GAME_CANVAS_ID = 'canvas-id';
export default function Home() {
const childRef = useRef<RefCacheCanvas>();
const initFunc = () => {
return new Promise((resolve) => {
const canvas: HTMLCanvasElement | null = document.getElementById(GAME_CANVAS_ID) as HTMLCanvasElement;
if (canvas && typeof canvas.getContext === 'function') {
let ctx: CanvasRenderingContext2D | null = canvas.getContext('2d');
ctx?.fillRect(25, 25, 100, 100);
ctx?.clearRect(45, 45, 60, 60);
ctx?.strokeRect(50, 50, 50, 50);
}
setTimeout(() => {
console.log('canvas paint ready!');
resolve(true);
}, 5000);
});
};
return (
<>
<h2 className={styles.title}>Home Page</h2>
<CacheCanvas
ref={childRef}
id={GAME_CANVAS_ID}
init={initFunc}
fallback={() => <div>fallback</div>}
/>
<button
style={{ display: 'block' }}
onClick={() => {
console.log('active cache!');
childRef.current?.cacheCanvasToStorage();
}}
>cache canvas</button>
</>
);
}
export const pageConfig = definePageConfig(() => {
return {
title: 'Home',
meta: [
{
name: 'theme-color',
content: '#000',
},
{
name: 'title-color',
content: '#f00',
},
],
auth: ['admin'],
};
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -0,0 +1,3 @@
.title {
color: red;
}

View File

@ -0,0 +1,3 @@
export interface AppData {
title: string;
}

View File

@ -0,0 +1 @@
/// <reference types="@ice/app/types" />

View File

@ -0,0 +1,32 @@
{
"compileOnSave": false,
"buildOnSave": false,
"compilerOptions": {
"baseUrl": ".",
"outDir": "build",
"module": "esnext",
"target": "es6",
"jsx": "react-jsx",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"lib": ["es6", "dom"],
"sourceMap": true,
"allowJs": true,
"rootDir": "./",
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": false,
"importHelpers": true,
"strictNullChecks": true,
"suppressImplicitAnyIndexErrors": true,
"noUnusedLocals": true,
"skipLibCheck": true,
"paths": {
"@/*": ["./src/*"],
"ice": [".ice"]
}
},
"include": ["src", ".ice", "ice.config.*"],
"exclude": ["build", "public"]
}

View File

@ -0,0 +1,46 @@
# @ice/cache-canvas
React component for supporting canvas for cache.
## Usage
```bash
npm i @ice/cache-canvas -S
```
```jsx
import MainGame from './game'; // eva.js 的封装
const GAME_CANVAS = 'game-canvas';
export default (props) => {
useEffect(() => {
const gameEl = document.getElementById(GAME_CANVAS);
new MainGame(gameEl, getGameHeight());
}, []);
const init = () => {
return new Promise((resolve) => {
const canvas: HTMLCanvasElement | null = document.getElementById(GAME_CANVAS_ID) as HTMLCanvasElement;
if (canvas && typeof canvas.getContext === 'function') {
let ctx: CanvasRenderingContext2D | null = canvas.getContext('2d');
ctx?.fillRect(25, 25, 100, 100);
ctx?.clearRect(45, 45, 60, 60);
ctx?.strokeRect(50, 50, 50, 50);
}
setTimeout(() => {
console.log('canvas paint ready!');
resolve(true);
}, 5000);
});
}
return (
<>
<CanvasCache id={GAME_CANVAS} useCache={false} init={init} />
</>
);
};
```

View File

@ -0,0 +1,29 @@
{
"name": "@ice/cache-canvas",
"version": "0.0.8",
"description": "",
"main": "./esm/index.js",
"types": "./esm/index.d.ts",
"scripts": {
"watch": "tsc -w",
"build": "tsc",
"prepublishOnly": "npm run build"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"dependencies": {
"universal-env": "^3.3.3"
},
"peerDependencies": {
"react": "^18",
"react-dom": "^18"
},
"publishConfig": {
"access": "public"
}
}

View File

@ -0,0 +1,161 @@
import { useEffect, useState, useImperativeHandle, forwardRef, useCallback } from 'react';
import type {
HTMLAttributes,
ReactElement,
} from 'react';
import * as React from 'react';
import { isNode } from 'universal-env';
import { Storage } from './storage';
declare global {
interface ImportMeta {
// The build target for ice.js
// Usually `web` or `node` or `weex`
target: string;
// The renderer for ice.js
renderer: 'client' | 'server';
// ice.js defined env variables
env: Record<string, string>;
}
interface Window {
WindVane: {
call: Function;
};
_windvane_backControl: Function | null;
}
}
export type RefCacheCanvas = {
// Call the API to store the canvas in storage.
cacheCanvasToStorage: () => void;
};
export type CacheCanvasProps = {
id: string;
init: () => Promise<any>;
useCache?: Boolean;
getSnapshot?: () => String;
fallback?: ReactElement;
style?: HTMLAttributes;
className?: HTMLAttributes;
};
export const CacheCanvas = forwardRef((props: CacheCanvasProps, ref) => {
const {
id,
init,
useCache = true,
getSnapshot,
fallback,
style,
className,
...rest
} = props;
const [renderedCanvas, setRenderedCanvas] = useState(!useCache);
const [mounted, setMounted] = useState(false);
const cacheKey = `cache-canvas-${id}`;
const cacheCanvasFunc = useCallback(() => {
// Cache base64 string of canvas.
const canvas: HTMLCanvasElement | null = document.getElementById(id) as HTMLCanvasElement;
let strBase64;
if (typeof getSnapshot === 'function') {
strBase64 = getSnapshot();
} else {
strBase64 = canvas.toDataURL();
}
// Cache base64 string when canvas rendered.
if (renderedCanvas && strBase64) {
Storage.setItem(cacheKey, strBase64);
}
}, [id, renderedCanvas, cacheKey, getSnapshot]);
useImperativeHandle(ref, () => ({
cacheCanvasToStorage: cacheCanvasFunc,
}));
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
if (window.WindVane) {
window.WindVane.call('WebAppInterface', 'enableHookNativeBack', {});
window._windvane_backControl = () => {
cacheCanvasFunc();
// Windvane must return a string value of true for it to work properly.
return 'true';
};
}
document.addEventListener('wvBackClickEvent', cacheCanvasFunc, false);
window.addEventListener('beforeunload', cacheCanvasFunc);
return () => {
window.removeEventListener('beforeunload', cacheCanvasFunc);
window.removeEventListener('wvBackClickEvent', cacheCanvasFunc);
if (window._windvane_backControl) {
window._windvane_backControl = null;
}
};
}, [cacheCanvasFunc]);
useEffect(() => {
if (mounted && typeof init === 'function') {
const res = init();
if (res instanceof Promise) {
res.then(() => {
setRenderedCanvas(true);
});
}
}
}, [mounted, init]);
return (
<>
<canvas
{...rest}
className={className}
style={renderedCanvas ? style : { ...style, display: 'none' }}
id={id}
/>
{
!renderedCanvas && (<>
<img
className={className}
style={style}
src={Storage.getItem(cacheKey) || ''}
id={`canvas-img-${id}`}
/>
{
(typeof fallback === 'function') && (<div
id={`fallback-${id}`}
style={isNode || Storage.getItem(cacheKey) ? { display: 'none' } : { display: 'block' }}
>
{
fallback()
}
</div>)
}
<script
dangerouslySetInnerHTML={{
__html: `
const base64Data = localStorage.getItem('${cacheKey}');
const fallback = document.getElementById('fallback-${id}');
if (base64Data) {
const img = document.getElementById('canvas-img-${id}');
img && (img.src = base64Data);
fallback && (fallback.style.display = 'none');
} else {
fallback && (fallback.style.display = 'block');
}
`,
}}
/>
</>)
}
</>
);
});

View File

@ -0,0 +1,26 @@
const cache = {};
export const Storage = {
setItem: (key, value) => {
try {
if (typeof window === 'object' && window.localStorage) {
return localStorage.setItem(key, value);
}
return (cache[key] = value);
} catch (e) {
console.error('Storage setItem error:', e);
}
},
getItem: (key) => {
try {
if (typeof window === 'object' && window.localStorage) {
return localStorage.getItem(key);
}
return cache[key] || '';
} catch (e) {
console.error('Storage getItem error:', e);
}
},
};

View File

@ -0,0 +1,3 @@
export function isFunction(obj: any): obj is Function {
return typeof obj === 'function';
}

View File

@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": "./",
"rootDir": "src",
"outDir": "esm"
},
"allowJs": true,
"include": [
"src"
]
}

View File

@ -0,0 +1,16 @@
# @ice/plugin-canvas
An ice.js plugin for canvas projects.
## Usage
Add plugin in `ice.config.mts`:
```js
import { defineConfig } from 'ice';
import canvasPlugin from '@ice/plugin-canvas';
export default defineConfig(() => ({
plugins: [canvasPlugin({ /* options */ })],
}));
```

View File

@ -0,0 +1,49 @@
{
"name": "@ice/plugin-canvas",
"version": "0.0.2",
"description": "Provide canvas render support for ice.js",
"license": "MIT",
"type": "module",
"exports": {
".": {
"import": "./esm/index.js",
"default": "./esm/index.js"
},
"./CacheCanvas": {
"types": "./esm/CacheCanvas.d.ts",
"import": "./esm/CacheCanvas.js",
"default": "./esm/CacheCanvas.js"
},
"./runtime": {
"types": "./esm/runtime.d.ts",
"import": "./esm/runtime.js",
"default": "./esm/runtime.js"
},
"./*": "./*"
},
"main": "./esm/index.js",
"types": "./esm/index.d.ts",
"files": [
"esm",
"!esm/**/*.map"
],
"dependencies": {
"@ice/cache-canvas": "workspace:*",
"@ice/runtime": "^1.2.4"
},
"devDependencies": {
"@ice/app": "^3.2.1",
"webpack": "^5.86.0"
},
"repository": {
"type": "http",
"url": "https://github.com/alibaba/ice/tree/master/packages/plugin-canvas"
},
"scripts": {
"watch": "tsc -w",
"build": "tsc"
},
"publishConfig": {
"access": "public"
}
}

View File

@ -0,0 +1,15 @@
import type { Plugin } from '@ice/app/types';
const PLUGIN_NAME = '@ice/plugin-canvas';
const plugin: Plugin = () => ({
name: PLUGIN_NAME,
setup: async ({ generator }) => {
generator.addExport({
source: '@ice/cache-canvas',
specifier: ['CacheCanvas'],
});
},
});
export default plugin;

View File

@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": "./",
"rootDir": "src",
"outDir": "esm",
"jsx": "react"
},
"include": ["src"]
}

View File

@ -134,6 +134,29 @@ importers:
speed-measure-webpack-plugin: 1.5.0_webpack@5.86.0
webpack: 5.86.0
examples/cavans-project:
specifiers:
'@ice/app': workspace:*
'@ice/cache-canvas': workspace:*
'@ice/plugin-canvas': workspace:*
'@ice/runtime': workspace:*
'@types/react': ^18.0.0
'@types/react-dom': ^18.0.0
react: ^18.0.0
react-dom: ^18.0.0
webpack: ^5.86.0
dependencies:
'@ice/app': link:../../packages/ice
'@ice/cache-canvas': link:../../packages/cache-canvas
'@ice/plugin-canvas': link:../../packages/plugin-cavans
'@ice/runtime': link:../../packages/runtime
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
devDependencies:
'@types/react': 18.0.34
'@types/react-dom': 18.0.11
webpack: 5.86.0
examples/csr-project:
specifiers:
'@ice/app': workspace:*
@ -1034,6 +1057,17 @@ importers:
webpack-dev-server: 4.15.0_webpack@5.86.0
ws: 8.12.1
packages/cache-canvas:
specifiers:
react: ^18.0.0
react-dom: ^18.0.0
universal-env: ^3.3.3
dependencies:
universal-env: 3.3.3
devDependencies:
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
packages/create-ice:
specifiers:
'@iceworks/generate-project': ^2.0.2
@ -1238,6 +1272,19 @@ importers:
'@types/react-dom': 18.0.11
regenerator-runtime: 0.13.11
packages/plugin-cavans:
specifiers:
'@ice/app': ^3.2.1
'@ice/cache-canvas': workspace:*
'@ice/runtime': ^1.2.4
webpack: ^5.86.0
dependencies:
'@ice/cache-canvas': link:../cache-canvas
'@ice/runtime': link:../runtime
devDependencies:
'@ice/app': link:../ice
webpack: 5.86.0
packages/plugin-css-assets-local:
specifiers:
'@ice/app': ^3.1.2
@ -9670,8 +9717,8 @@ packages:
engines: {node: '>=10'}
hasBin: true
dependencies:
JSONStream: 1.3.5
is-text-path: 1.0.1
JSONStream: 1.3.5
lodash: 4.17.21
meow: 8.1.2
split2: 3.2.2
@ -18677,12 +18724,6 @@ packages:
/react-dev-utils/12.0.1_y75l3d5in5mgvug53qfq62ncxu:
resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==}
engines: {node: '>=14'}
peerDependencies:
typescript: '>=2.7'
webpack: '>=4'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
'@babel/code-frame': 7.18.6
address: 1.2.2
@ -18713,7 +18754,9 @@ packages:
transitivePeerDependencies:
- eslint
- supports-color
- typescript
- vue-template-compiler
- webpack
/react-dom/17.0.2_react@17.0.2:
resolution: {integrity: sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==}