Skip to content

Commit

Permalink
feat: implement plugin-model
Browse files Browse the repository at this point in the history
  • Loading branch information
sorrycc committed Feb 3, 2020
1 parent 5b92aa2 commit 734e5d6
Show file tree
Hide file tree
Showing 17 changed files with 518 additions and 1 deletion.
1 change: 1 addition & 0 deletions example/.umirc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ export default defineConfig({
require.resolve('../packages/plugin-antd/lib'),
require.resolve('../packages/plugin-dva/lib'),
require.resolve('../packages/plugin-locale/lib'),
require.resolve('../packages/plugin-model/lib'),
],
});
5 changes: 5 additions & 0 deletions example/models/bar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default function() {
return {
description: 'bar',
};
}
7 changes: 7 additions & 0 deletions example/pages/plugin-model.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

.normal {
}

.title {
background: rgb(121, 207, 242);
}
12 changes: 12 additions & 0 deletions example/pages/plugin-model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';
import { useModel } from 'umi';
import styles from './plugin-model.css';

export default () => {
const bar = useModel('bar');
return (
<div>
<h1 className={styles.title}>Page plugin-model {bar.description}</h1>
</div>
);
};
5 changes: 4 additions & 1 deletion packages/plugin-model/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,8 @@
],
"license": "MIT",
"bugs": "http://github.com/umijs/plugins/issues",
"homepage": "https://github.com/umijs/plugins/tree/master/packages/plugin-model#readme"
"homepage": "https://github.com/umijs/plugins/tree/master/packages/plugin-model#readme",
"dependencies": {
"lodash.isequal": "4.5.0"
}
}
1 change: 1 addition & 0 deletions packages/plugin-model/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const DIR_NAME_IN_TMP = 'plugin-model';
3 changes: 3 additions & 0 deletions packages/plugin-model/src/helpers/constant.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import React from 'react';

export const UmiContext = React.createContext({});
18 changes: 18 additions & 0 deletions packages/plugin-model/src/helpers/dispatcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export default class Dispatcher {
callbacks = {};

data = {};

update = (namespace: string) => {
(this.callbacks[namespace] || []).forEach(
(callback: (val: any) => void) => {
try {
const data = this.data[namespace];
callback(data);
} catch (e) {
callback(undefined);
}
},
);
};
}
22 changes: 22 additions & 0 deletions packages/plugin-model/src/helpers/executor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';

interface ExecutorProps {
hook: () => any;
onUpdate: (val: any) => void;
namespace: string;
}

export default (props: ExecutorProps) => {
const { hook, onUpdate, namespace } = props;
try {
const data = hook();
onUpdate(data);
} catch (e) {
console.error(
`plugin-model: Invoking '${namespace || 'unknown'}' model failed:`,
e,
);
}

return <></>;
};
55 changes: 55 additions & 0 deletions packages/plugin-model/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { join } from 'path';
import { IApi } from 'umi';
import { DIR_NAME_IN_TMP } from './constants';
import getProviderContent from './utils/getProviderContent';
import getUseModelContent from './utils/getUseModelContent';

export default (api: IApi) => {
const {
paths,
utils: { winPath },
} = api;

function getModelsPath() {
return join(paths.absSrcPath!, api.config.singular ? 'model' : 'models');
}

// Add provider wrapper with rootContainer
api.addRuntimePlugin(() => join(winPath(__dirname), './runtime'));

api.onGenerateFiles(async () => {
const modelsPath = getModelsPath();
try {
const additionalModels = await api.applyPlugins({
key: 'addExtraModels',
type: api.ApplyPluginsType.add,
initialValue: [],
});
// Write models/provider.tsx
api.writeTmpFile({
path: `${DIR_NAME_IN_TMP}/Provider.tsx`,
content: getProviderContent(modelsPath, additionalModels),
});
// Write models/useModel.tsx
api.writeTmpFile({
content: getUseModelContent(),
path: `${DIR_NAME_IN_TMP}/useModel.tsx`,
});
} catch (e) {
console.error(e);
}
});

api.addTmpGenerateWatcherPaths(() => {
const modelsPath = getModelsPath();
return [modelsPath];
});

// Export useModel and Models from umi
api.addUmiExports(() => [
{
exportAll: true,
source: winPath(join(paths.absTmpPath!, DIR_NAME_IN_TMP, 'useModel')),
},
]);
};
12 changes: 12 additions & 0 deletions packages/plugin-model/src/runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* eslint-disable import/no-dynamic-require */
import React from 'react';
import { DIR_NAME_IN_TMP } from './constants';

export function rootContainer(container: React.ReactNode) {
return React.createElement(
// eslint-disable-next-line global-require
require(`@@/${DIR_NAME_IN_TMP}/Provider`).default,
null,
container,
);
}
105 changes: 105 additions & 0 deletions packages/plugin-model/src/utils/getProviderContent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { join } from 'path';
import { EOL } from 'os';
import { utils } from 'umi';

import {
genImports,
genModels,
genExtraModels,
ModelItem,
getValidFiles,
} from './index';

const { winPath } = utils;

function getFiles(cwd: string) {
return utils.glob
.sync('./**/*.{ts,tsx,js,jsx}', {
cwd,
})
.filter(
(file: string) =>
!file.endsWith('.d.ts') &&
!file.endsWith('.test.js') &&
!file.endsWith('.test.jsx') &&
!file.endsWith('.test.ts') &&
!file.endsWith('.test.tsx'),
);
}

function getModels(files: string[]) {
const sortedModels = genModels(files);
return sortedModels
.map(ele => `'${ele.namespace.replace(/'/g, "\\'")}': ${ele.importName}`)
.join(', ');
}

function getExtraModels(models: ModelItem[] = []) {
const extraModels = genExtraModels(models);
return extraModels
.map(ele => `'${ele.namespace}': ${winPath(ele.importName)}`)
.join(', ');
}

function getExtraImports(models: ModelItem[] = []) {
const extraModels = genExtraModels(models);
return extraModels
.map(
ele =>
`import ${ele.importName} from '${winPath(
ele.importPath.replace(/'/g, "\\'"),
)}';`,
)
.join(EOL);
}

export default function(modelsDir: string, extra: ModelItem[] = []) {
const files = getValidFiles(getFiles(modelsDir), modelsDir);
const imports = genImports(files);
const userModels = getModels(files);
const extraModels = getExtraModels(extra);
const extraImports = getExtraImports(extra);
return `import React from 'react';
${extraImports}
${imports}
// @ts-ignore
import Dispatcher from '${winPath(
join(__dirname, '..', 'helpers', 'dispatcher'),
)}';
// @ts-ignore
import Executor from '${winPath(join(__dirname, '..', 'helpers', 'executor'))}';
// @ts-ignore
import { UmiContext } from '${winPath(
join(__dirname, '..', 'helpers', 'constant'),
)}';
export const models = { ${extraModels ? `${extraModels}, ` : ''}${userModels} };
export type Model<T extends keyof typeof models> = {
[key in keyof typeof models]: ReturnType<typeof models[T]>;
};
export type Models<T extends keyof typeof models> = Model<T>[T]
const dispatcher = new Dispatcher!();
const Exe = Executor!;
export default ({ children }: { children: React.ReactNode }) => {
return (
<UmiContext.Provider value={dispatcher}>
{
Object.entries(models).map(pair => (
<Exe key={pair[0]} namespace={pair[0]} hook={pair[1] as any} onUpdate={(val: any) => {
const [ns] = pair as [keyof typeof models, any];
dispatcher.data[ns] = val;
dispatcher.update(ns);
}} />
))
}
{children}
</UmiContext.Provider>
)
}
`;
}
61 changes: 61 additions & 0 deletions packages/plugin-model/src/utils/getUseModelContent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { join } from 'path';
import { utils } from 'umi';

const { winPath } = utils;

export default function() {
return `import { useState, useEffect, useContext, useRef } from 'react';
// @ts-ignore
import isEqual from '${winPath(require.resolve('lodash.isequal'))}';
// @ts-ignore
import { UmiContext } from '${winPath(
join(__dirname, '..', 'helpers', 'constant'),
)}';
import { Model, models } from './Provider';
export type Models<T extends keyof typeof models> = Model<T>[T]
export function useModel<T extends keyof Model<T>>(model: T): Model<T>[T]
export function useModel<T extends keyof Model<T>, U>(model: T, selector: (model: Model<T>[T]) => U): U
export function useModel<T extends keyof Model<T>, U>(
namespace: T,
updater?: (model: Model<T>[T]) => U
) : typeof updater extends undefined ? Model<T>[T] : ReturnType<NonNullable<typeof updater>>{
type RetState = typeof updater extends undefined ? Model<T>[T] : ReturnType<NonNullable<typeof updater>>
const dispatcher = useContext<any>(UmiContext);
const updaterRef = useRef(updater);
updaterRef.current = updater;
const [state, setState] = useState<RetState>(
() => updaterRef.current ? updaterRef.current(dispatcher.data![namespace]) : dispatcher.data![namespace]
);
const lastState = useRef<any>(state);
useEffect(() => {
const handler = (e: any) => {
if(updater && updaterRef.current){
const ret = updaterRef.current(e);
if(!isEqual(ret, lastState.current)){
lastState.current = ret;
setState(ret);
}
} else {
setState(e);
}
}
try {
dispatcher.callbacks![namespace]!.add(handler);
} catch (e) {
dispatcher.callbacks![namespace] = new Set();
dispatcher.callbacks![namespace]!.add(handler);
}
return () => {
dispatcher.callbacks![namespace]!.delete(handler);
}
}, [namespace]);
return state;
};
`;
}
Loading

0 comments on commit 734e5d6

Please sign in to comment.