perf: 优化话题更新逻辑

并增加了zustand作为插件推荐的解决方案
pull/56/head
moonrailgun 3 years ago
parent c5b79ae603
commit 5a126eb9e7

@ -1,7 +1,7 @@
const path = require('path'); const path = require('path');
module.exports = { module.exports = {
externalDeps: ['react', 'styled-components'], externalDeps: ['react', 'react-router', 'axios', 'styled-components', 'zustand', 'zustand/middleware/immer'],
pluginRoot: path.resolve(__dirname, './web'), pluginRoot: path.resolve(__dirname, './web'),
outDir: path.resolve(__dirname, '../../public'), outDir: path.resolve(__dirname, '../../public'),
}; };

@ -1,7 +1,7 @@
const path = require('path'); const path = require('path');
module.exports = { module.exports = {
externalDeps: ['react', 'styled-components'], externalDeps: ['react', 'react-router', 'axios', 'styled-components', 'zustand', 'zustand/middleware/immer'],
pluginRoot: path.resolve(__dirname, './web'), pluginRoot: path.resolve(__dirname, './web'),
outDir: path.resolve(__dirname, '../../public'), outDir: path.resolve(__dirname, '../../public'),
}; };

@ -72,7 +72,7 @@ export class AppSocket {
*/ */
removeListener(eventName: string, callback: (data: any) => void) { removeListener(eventName: string, callback: (data: any) => void) {
const index = this.listener.findIndex( const index = this.listener.findIndex(
(item) => item[0] === eventName && item[1] === callback (item) => item[0] === `notify:${eventName}` && item[1] === callback
); );
if (index >= 0) { if (index >= 0) {
this.listener.splice(index, 1); this.listener.splice(index, 1);

@ -63,7 +63,8 @@
"tailchat-shared": "*", "tailchat-shared": "*",
"tailwindcss": "^2.2.4", "tailwindcss": "^2.2.4",
"url": "^0.11.0", "url": "^0.11.0",
"yup": "^0.32.9" "yup": "^0.32.9",
"zustand": "^4.1.2"
}, },
"devDependencies": { "devDependencies": {
"@testing-library/jest-dom": "^5.14.1", "@testing-library/jest-dom": "^5.14.1",

@ -1,4 +1,4 @@
import { Icon } from '@/components/Icon'; import { Icon } from 'tailchat-design';
import { Dropdown } from 'antd'; import { Dropdown } from 'antd';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { useChatInputActionContext } from './context'; import { useChatInputActionContext } from './context';

@ -16,6 +16,11 @@ function registerDependencies() {
regDependency('react-router', () => import('react-router')); regDependency('react-router', () => import('react-router'));
regDependency('axios', () => import('axios')); // 用于插件的第三方包使用axios作为依赖的情况下可以减少包体积 regDependency('axios', () => import('axios')); // 用于插件的第三方包使用axios作为依赖的情况下可以减少包体积
regDependency('styled-components', () => import('styled-components')); // 仅用于第三方插件. tailchat本身更多使用 tailwindcss regDependency('styled-components', () => import('styled-components')); // 仅用于第三方插件. tailchat本身更多使用 tailwindcss
regDependency('zustand', () => import('zustand')); // 仅用于第三方插件. tailchat本身更多使用 tailwindcss
regDependency(
'zustand/middleware/immer',
() => import('zustand/middleware/immer')
); // 仅用于第三方插件. tailchat本身更多使用 tailwindcss
} }
function registerModules() { function registerModules() {

@ -32,6 +32,7 @@ class PluginManager {
await initMiniStar({ await initMiniStar({
plugins, plugins,
onPluginLoadError: (err) => { onPluginLoadError: (err) => {
console.error('Plugin load error:', err);
loadErrorPlugins.add(err.pluginName); loadErrorPlugins.add(err.pluginName);
}, },
}); });

@ -373,6 +373,7 @@ importers:
webpackbar: ^5.0.2 webpackbar: ^5.0.2
workbox-webpack-plugin: ^6.5.1 workbox-webpack-plugin: ^6.5.1
yup: ^0.32.9 yup: ^0.32.9
zustand: ^4.1.2
dependencies: dependencies:
'@loadable/component': 5.15.2_react@18.2.0 '@loadable/component': 5.15.2_react@18.2.0
ahooks: 3.7.1_react@18.2.0 ahooks: 3.7.1_react@18.2.0
@ -415,6 +416,7 @@ importers:
tailwindcss: 2.2.19_ywsstkkounrjlah5ti55snp2aq tailwindcss: 2.2.19_ywsstkkounrjlah5ti55snp2aq
url: 0.11.0 url: 0.11.0
yup: 0.32.11 yup: 0.32.11
zustand: 4.1.2_react@18.2.0
devDependencies: devDependencies:
'@testing-library/jest-dom': 5.16.5 '@testing-library/jest-dom': 5.16.5
'@testing-library/react': 12.1.5_biqbaboplfbrettd7655fr4n2y '@testing-library/react': 12.1.5_biqbaboplfbrettd7655fr4n2y
@ -874,6 +876,12 @@ importers:
'@types/react-router': 5.1.18 '@types/react-router': 5.1.18
react: 18.2.0 react: 18.2.0
server/plugins/com.msgbyte.prettyinvite:
specifiers:
tailchat-server-sdk: '*'
dependencies:
tailchat-server-sdk: link:../../packages/sdk
server/plugins/com.msgbyte.simplenotify: server/plugins/com.msgbyte.simplenotify:
specifiers: specifiers:
mini-star: ^1.2.8 mini-star: ^1.2.8
@ -913,6 +921,7 @@ importers:
lodash: ^4.17.21 lodash: ^4.17.21
mini-star: '*' mini-star: '*'
nanoid: ^3.1.23 nanoid: ^3.1.23
rollup-plugin-inject-process-env: ^1.3.1
rollup-plugin-less: ^1.1.3 rollup-plugin-less: ^1.1.3
tailchat-server-sdk: '*' tailchat-server-sdk: '*'
dependencies: dependencies:
@ -923,6 +932,7 @@ importers:
'@types/react': 18.0.20 '@types/react': 18.0.20
less: 4.1.3 less: 4.1.3
mini-star: 1.3.1 mini-star: 1.3.1
rollup-plugin-inject-process-env: 1.3.1
rollup-plugin-less: 1.1.3 rollup-plugin-less: 1.1.3
server/plugins/com.msgbyte.topic/web/plugins/com.msgbyte.topic: server/plugins/com.msgbyte.topic/web/plugins/com.msgbyte.topic:
@ -930,10 +940,12 @@ importers:
'@types/styled-components': ^5.1.26 '@types/styled-components': ^5.1.26
react: 18.2.0 react: 18.2.0
styled-components: ^5.3.6 styled-components: ^5.3.6
zustand: ^4.1.2
devDependencies: devDependencies:
'@types/styled-components': 5.1.26 '@types/styled-components': 5.1.26
react: 18.2.0 react: 18.2.0
styled-components: 5.3.6_react@18.2.0 styled-components: 5.3.6_react@18.2.0
zustand: 4.1.2_react@18.2.0
website: website:
specifiers: specifiers:
@ -1345,7 +1357,7 @@ packages:
'@babel/compat-data': 7.18.13 '@babel/compat-data': 7.18.13
'@babel/core': 7.18.13 '@babel/core': 7.18.13
'@babel/helper-validator-option': 7.18.6 '@babel/helper-validator-option': 7.18.6
browserslist: 4.21.4 browserslist: 4.21.3
semver: 6.3.0 semver: 6.3.0
/@babel/helper-create-class-features-plugin/7.18.13_@babel+core@7.18.13: /@babel/helper-create-class-features-plugin/7.18.13_@babel+core@7.18.13:
@ -10818,7 +10830,7 @@ packages:
babel-plugin-syntax-jsx: 6.18.0 babel-plugin-syntax-jsx: 6.18.0
lodash: 4.17.21 lodash: 4.17.21
picomatch: 2.3.1 picomatch: 2.3.1
styled-components: 5.3.6_mdz3marskokvq6744hhidi3r5a styled-components: 5.3.6_react@18.2.0
/babel-plugin-syntax-jsx/6.18.0: /babel-plugin-syntax-jsx/6.18.0:
resolution: {integrity: sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==} resolution: {integrity: sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==}
@ -18586,7 +18598,7 @@ packages:
pretty-format: 27.5.1 pretty-format: 27.5.1
slash: 3.0.0 slash: 3.0.0
strip-json-comments: 3.1.1 strip-json-comments: 3.1.1
ts-node: 10.9.1_t4lrjbt3sxauai4t5o275zsepa ts-node: 10.9.1_bqee57coj3oib6dw4m24wknwqe
transitivePeerDependencies: transitivePeerDependencies:
- bufferutil - bufferutil
- canvas - canvas
@ -25012,6 +25024,7 @@ packages:
prop-types: 15.8.1 prop-types: 15.8.1
react: 16.14.0 react: 16.14.0
scheduler: 0.19.1 scheduler: 0.19.1
dev: false
/react-dom/17.0.2_react@17.0.2: /react-dom/17.0.2_react@17.0.2:
resolution: {integrity: sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==} resolution: {integrity: sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==}
@ -25731,6 +25744,7 @@ packages:
loose-envify: 1.4.0 loose-envify: 1.4.0
object-assign: 4.1.1 object-assign: 4.1.1
prop-types: 15.8.1 prop-types: 15.8.1
dev: false
/react/17.0.2: /react/17.0.2:
resolution: {integrity: sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==} resolution: {integrity: sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==}
@ -26556,6 +26570,12 @@ packages:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
/rollup-plugin-inject-process-env/1.3.1:
resolution: {integrity: sha512-kKDoL30IZr0wxbNVJjq+OS92RJSKRbKV6B5eNW4q3mZTFqoWDh6lHy+mPDYuuGuERFNKXkG+AKxvYqC9+DRpKQ==}
dependencies:
magic-string: 0.25.9
dev: true
/rollup-plugin-less/1.1.3: /rollup-plugin-less/1.1.3:
resolution: {integrity: sha512-gvJFXpEeU5Opyz514ZO4JGj9kvFTChZEDMR3LSkSIyFfWaeE5NJMFzxPpo+MZK3CY/0j7+AotDeRofyQt9rTew==} resolution: {integrity: sha512-gvJFXpEeU5Opyz514ZO4JGj9kvFTChZEDMR3LSkSIyFfWaeE5NJMFzxPpo+MZK3CY/0j7+AotDeRofyQt9rTew==}
dependencies: dependencies:
@ -26808,6 +26828,7 @@ packages:
dependencies: dependencies:
loose-envify: 1.4.0 loose-envify: 1.4.0
object-assign: 4.1.1 object-assign: 4.1.1
dev: false
/scheduler/0.20.2: /scheduler/0.20.2:
resolution: {integrity: sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==} resolution: {integrity: sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==}
@ -27977,6 +27998,7 @@ packages:
react-is: 16.13.1 react-is: 16.13.1
shallowequal: 1.1.0 shallowequal: 1.1.0
supports-color: 5.5.0 supports-color: 5.5.0
dev: false
/styled-components/5.3.6_react@18.2.0: /styled-components/5.3.6_react@18.2.0:
resolution: {integrity: sha512-hGTZquGAaTqhGWldX7hhfzjnIYBZ0IXQXkCYdvF1Sq3DsUaLx6+NTHC5Jj1ooM2F68sBiVz3lvhfwQs/S3l6qg==} resolution: {integrity: sha512-hGTZquGAaTqhGWldX7hhfzjnIYBZ0IXQXkCYdvF1Sq3DsUaLx6+NTHC5Jj1ooM2F68sBiVz3lvhfwQs/S3l6qg==}
@ -27998,7 +28020,6 @@ packages:
react: 18.2.0 react: 18.2.0
shallowequal: 1.1.0 shallowequal: 1.1.0
supports-color: 5.5.0 supports-color: 5.5.0
dev: true
/styled-system/5.1.5: /styled-system/5.1.5:
resolution: {integrity: sha512-7VoD0o2R3RKzOzPK0jYrVnS8iJdfkKsQJNiLRDjikOpQVqQHns/DXWaPZOH4tIKkhAT7I6wIsy9FWTWh2X3q+A==} resolution: {integrity: sha512-7VoD0o2R3RKzOzPK0jYrVnS8iJdfkKsQJNiLRDjikOpQVqQHns/DXWaPZOH4tIKkhAT7I6wIsy9FWTWh2X3q+A==}
@ -29026,6 +29047,7 @@ packages:
typescript: 4.7.4 typescript: 4.7.4
v8-compile-cache-lib: 3.0.1 v8-compile-cache-lib: 3.0.1
yn: 3.1.1 yn: 3.1.1
dev: false
/ts-pnp/1.2.0_typescript@4.7.4: /ts-pnp/1.2.0_typescript@4.7.4:
resolution: {integrity: sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==} resolution: {integrity: sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==}
@ -29819,7 +29841,6 @@ packages:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies: dependencies:
react: 18.2.0 react: 18.2.0
dev: false
/use/3.1.1: /use/3.1.1:
resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==} resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==}
@ -31201,7 +31222,6 @@ packages:
dependencies: dependencies:
react: 18.2.0 react: 18.2.0
use-sync-external-store: 1.2.0_react@18.2.0 use-sync-external-store: 1.2.0_react@18.2.0
dev: false
/zwitch/1.0.5: /zwitch/1.0.5:
resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==} resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==}

@ -1,7 +1,12 @@
const path = require('path'); const path = require('path');
module.exports = { module.exports = {
externalDeps: ['react', 'styled-components'], externalDeps: [
'react',
'styled-components',
'zustand',
'zustand/middleware/immer',
],
pluginRoot: path.resolve(__dirname, './web'), pluginRoot: path.resolve(__dirname, './web'),
outDir: path.resolve(__dirname, '../../public'), outDir: path.resolve(__dirname, '../../public'),
}; };

@ -14,6 +14,7 @@
"@types/react": "18.0.20", "@types/react": "18.0.20",
"less": "^4.1.3", "less": "^4.1.3",
"mini-star": "*", "mini-star": "*",
"rollup-plugin-inject-process-env": "^1.3.1",
"rollup-plugin-less": "^1.1.3" "rollup-plugin-less": "^1.1.3"
}, },
"dependencies": { "dependencies": {

@ -10,6 +10,7 @@
"devDependencies": { "devDependencies": {
"@types/styled-components": "^5.1.26", "@types/styled-components": "^5.1.26",
"react": "18.2.0", "react": "18.2.0",
"styled-components": "^5.3.6" "styled-components": "^5.3.6",
"zustand": "^4.1.2"
} }
} }

@ -20,7 +20,11 @@ export const TopicCreate: React.FC<{
return ( return (
<ModalWrapper title={Translate.createBtn}> <ModalWrapper title={Translate.createBtn}>
<TextArea value={text} onChange={(e) => setText(e.target.value)} /> <TextArea
autoFocus
value={text}
onChange={(e) => setText(e.target.value)}
/>
<Footer> <Footer>
<Button type="primary" loading={loading} onClick={handleCreate}> <Button type="primary" loading={loading} onClick={handleCreate}>

@ -1,6 +1,7 @@
import React, { useCallback, useEffect } from 'react'; import React, { useCallback, useEffect } from 'react';
import { TopicCard } from '../components/TopicCard'; import { TopicCard } from '../components/TopicCard';
import { import {
showSuccessToasts,
useAsyncRequest, useAsyncRequest,
useGlobalSocketEvent, useGlobalSocketEvent,
useGroupPanelContext, useGroupPanelContext,
@ -17,6 +18,8 @@ import { request } from '../request';
import { Translate } from '../translate'; import { Translate } from '../translate';
import { TopicCreate } from '../components/modals/TopicCreate'; import { TopicCreate } from '../components/modals/TopicCreate';
import styled from 'styled-components'; import styled from 'styled-components';
import { useTopicStore } from '../store';
import type { GroupTopic } from '../types';
const Root = styled(LoadingOnFirst)({ const Root = styled(LoadingOnFirst)({
display: 'flex', display: 'flex',
@ -37,46 +40,68 @@ const Root = styled(LoadingOnFirst)({
const GroupTopicPanelRender: React.FC = React.memo(() => { const GroupTopicPanelRender: React.FC = React.memo(() => {
const panelInfo = useGroupPanelContext(); const panelInfo = useGroupPanelContext();
const { panelId, groupId } = panelInfo;
const { topicMap, addTopicPanel, addTopicItem, updateTopicItem } =
useTopicStore();
const topicList = topicMap[panelId];
const [{ value: list = [], loading }, fetch] = useAsyncRequest(async () => { const [{ loading }, fetch] = useAsyncRequest(async () => {
if (!panelInfo.groupId || !panelInfo.panelId) { if (!groupId || !panelId) {
return []; return [];
} }
const { data } = await request.get('list', { const { data } = await request.get('list', {
params: { params: {
groupId: panelInfo.groupId, groupId,
panelId: panelInfo.panelId, panelId,
}, },
}); });
return data; addTopicPanel(panelId, data);
}, [panelInfo.groupId, panelInfo.panelId]); }, [groupId, panelId, addTopicPanel]);
useEffect(() => { useEffect(() => {
/**
*
*/
fetch(); fetch();
}, [fetch]); }, [fetch]);
useGlobalSocketEvent('plugin:com.msgbyte.topic.create', () => { useGlobalSocketEvent(
fetch(); // not good, 待优化 'plugin:com.msgbyte.topic.create',
}); (topic: GroupTopic) => {
/**
*
*/
if (topic.panelId === panelId) {
addTopicItem(panelId, topic);
}
}
);
useGlobalSocketEvent('plugin:com.msgbyte.topic.createComment', () => { useGlobalSocketEvent(
fetch(); // not good, 待优化 'plugin:com.msgbyte.topic.createComment',
}); (topic: GroupTopic) => {
/**
*
*/
if (topic.panelId === panelId) {
updateTopicItem(panelId, topic);
}
}
);
const handleCreateTopic = useCallback(() => { const handleCreateTopic = useCallback(() => {
const key = openModal( const key = openModal(
<TopicCreate <TopicCreate
onCreate={async (text) => { onCreate={async (text) => {
await request.post('create', { await request.post('create', {
groupId: panelInfo.groupId, groupId,
panelId: panelInfo.panelId, panelId,
content: text, content: text,
}); });
fetch(); showSuccessToasts();
closeModal(key); closeModal(key);
}} }}
/> />
@ -85,8 +110,8 @@ const GroupTopicPanelRender: React.FC = React.memo(() => {
return ( return (
<Root spinning={loading}> <Root spinning={loading}>
{Array.isArray(list) && list.length > 0 ? ( {Array.isArray(topicList) && topicList.length > 0 ? (
list.map((item, i) => <TopicCard key={i} topic={item} />) topicList.map((item, i) => <TopicCard key={i} topic={item} />)
) : ( ) : (
<Empty description={Translate.noTopic}> <Empty description={Translate.noTopic}>
<Button type="primary" onClick={handleCreateTopic}> <Button type="primary" onClick={handleCreateTopic}>

@ -0,0 +1,48 @@
import create from 'zustand';
import { immer } from 'zustand/middleware/immer';
import type { GroupTopic } from './types';
interface TopicPanelMap {
[panelId: string]: GroupTopic[];
}
interface TopicStoreState {
topicMap: TopicPanelMap;
addTopicPanel: (panelId: string, topicList: GroupTopic[]) => void;
addTopicItem: (panelId: string, topic: GroupTopic) => void;
updateTopicItem: (panelId: string, topic: GroupTopic) => void;
}
export const useTopicStore = create<
TopicStoreState,
[['zustand/immer', never]]
>(
immer((set) => ({
topicMap: {},
addTopicPanel: (panelId, topicList) => {
set((state) => {
state.topicMap[panelId] = topicList;
});
},
addTopicItem: (panelId, topic) => {
set((state) => {
if (state.topicMap[panelId]) {
state.topicMap[panelId].unshift(topic);
}
});
},
updateTopicItem: (panelId, topic) => {
set((state) => {
if (state.topicMap[panelId]) {
const findedTopicIndex = state.topicMap[panelId].findIndex(
(t) => t._id === topic._id
);
if (findedTopicIndex >= 0) {
state.topicMap[panelId][findedTopicIndex] = topic;
}
}
});
},
}))
);
Loading…
Cancel
Save