English | 中文 |
Remesh, 基于 CQRS 的领域驱动设计框架, 为大型 TypeScript/JavasScript 应用而生.
remesh/modules/history
)所以, 为什么是 Remesh 呢? 它能为我的应用带来何种益处呢?
这取决于你是否被以下内容所吸引:
模块化: 你不必将你的状态聚合到一起, 它可以原子化的方式被定义和处理, 并可以 domain.query
聚合为其它的派生状态.
高性能: 如果没有订阅, 你的组件不会因 domain.query
的变化而重新渲染.
可维护性: Remesh 提供了一组极具表达能力的 APIs, 鼓励你通过规范的编码方式, 维护业务逻辑, 如此提高代码的可维护性.
可组合性: 无需仅为你的多个页面构建单一的 domain, 你可以按需定义多个 domain, 并且在一个 domain 中通过 domain.getDomain(...)
来访问其它 domains.
复用性: 你可以编写 remesh 自定义模块以在多个 domains 中复用逻辑, 就像编写 react-hooks 那样.
可测试性: 你的 Remesh code 是视图无关的, 因此你可以在测试环境下更简单地测试你的业务逻辑.
可预测性: Remesh 帮助你将业务逻辑划分为 pure 和 effect 部分: pure 部分是纯函数和不可变数据, 它们安全且可预测, 并且构成了你业务逻辑的核心. effect 部分则通过 rxjs 以组合的方式管理副作用, 因此我们可以轻松的控制数据的流向.
可持续性: 你的业务逻辑并不和你的视图层绑定, 即便你从一个视图库迁移到另一个 (比如从 react 到 vue), 你仍然可以重用所有的 remesh 代码, 并且无需重构或重写即可继续迭代.
一个 domain 就像你的应用中的一个 component, 但它不关于 UI, 而是关于你的业务逻辑.
“所有相关的事物被封装到一起”, 即为 domain.
一个 domain 可以根据你的需求, 包含许多种资源, 如下:
Domain Effects: 一个可观察对象(observable), 用于执行副作用, 发送 commands 或者 events.
对于任意 domain 而言, 只有 domain-query
, domain-command
, domain-event
可以被暴露出去.
domain-state
既不会被暴露出去, 也不能在 domain 以外被直接接触到.
对于 domains 的消费方而言.
唯一读取 states 的方式, 是 domain-query
, 以此阻止那些无效的读取.
唯一更新 states 的方式, 是 domain-command
, 以此阻止那些无效的更新.
# Install remesh and rxjs via npm
npm install --save remesh rxjs
# Install remesh and rxjs via yarn
yarn add remesh rxjs
你可以在 stackblitz 上编辑它.
// domain.ts
import { Remesh } from 'remesh'
import { interval } from 'rxjs'
import { map, switchMap, takeUntil } from 'rxjs/operators'
type ChangeMode = 'increment' | 'decrement'
/**
* Define your domain model
*/
export const CountDomain = Remesh.domain({
name: 'CountDomain',
impl: (domain) => {
/**
* Define your domain's related states
*/
const CountState = domain.state({
name: 'CountState',
default: 0,
})
/**
* Define your domain's related events
*/
const CountChangedEvent = domain.event<number>({
name: 'CountChangedEvent',
})
/**
* Define your domain's related commands
*/
const SetCountCommand = domain.command({
name: 'SetCountCommand',
impl: ({}, count: number) => {
/**
* Update the domain's state and emit the related event
*/
return [CountState().new(count), CountChangedEvent(count)]
},
})
/**
* Define your domain's related queries
*/
const CountQuery = domain.query({
name: 'CountQuery',
impl: ({ get }) => {
/**
* Get the domain's state
*/
return get(CountState())
},
})
/**
* You can use a command in another command
*/
const IncreaseCountCommand = domain.command({
name: 'IncreaseCountCommand',
impl: ({ get }, count: number = 1) => {
return SetCountCommand(get(CountState()) + count)
},
})
/**
* You can use a command in another command
*/
const DecreaseCountCommand = domain.command({
name: 'DecreaseCountCommand',
impl: ({ get }, count: number = 1) => {
return SetCountCommand(get(CountState()) - count)
},
})
const ChangeCountByModeCommand = domain.command({
name: 'ChangeCountByModeCommand',
impl: ({}, mode: ChangeMode) => {
if (mode === 'increment') return IncreaseCountCommand()
if (mode === 'decrement') return DecreaseCountCommand()
return null
},
})
/**
* Define an event for starting increment or decrement periodically
*/
const StartEvent = domain.event<ChangeMode>({
name: 'StartEvent',
})
/**
* Define a command to send event since event can't be sended outside of domain
*/
const StartCommand = domain.command({
name: 'StartCommand',
impl: ({}, mode: ChangeMode) => {
return StartEvent(mode)
},
})
/**
* Define an event for stopping signal
*/
const StopEvent = domain.event({
name: 'StopEvent',
})
/**
* Define a command to send event since event can't be sended outside of domain
*/
const StopCommand = domain.command({
name: 'StopCommand',
impl: () => {
return StopEvent()
},
})
/**
* Define your domain's related effects
*/
domain.effect({
name: 'ChangeCountEffect',
impl: ({ fromEvent }) => {
return fromEvent(StartEvent).pipe(
switchMap((mode) => {
return interval(100).pipe(
map(() => ChangeCountByModeCommand(mode)),
// finished when received stop event
takeUntil(fromEvent(StopEvent)),
)
}),
)
},
})
/**
* Expose domain resources
*/
return {
query: {
CountQuery,
},
command: {
SetCountCommand,
IncreaseCountCommand,
DecreaseCountCommand,
StartCommand,
StopCommand,
},
event: {
StartEvent,
StopEvent,
CountChangedEvent,
},
}
},
})
// index.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import * as React from 'react'
import { RemeshRoot, useRemeshDomain, useRemeshQuery, useRemeshSend, useRemeshEvent } from 'remesh-react'
import { CountDomain } from './domain'
export const Counter = () => {
/**
* use remesh send for sending commands
*/
const send = useRemeshSend()
/**
* read domain via useRemeshDomain
*/
const countDomain = useRemeshDomain(CountDomain())
/**
* read domain query via useRemeshQuery
*/
const count = useRemeshQuery(countDomain.query.CountQuery())
const handleIncrement = () => {
/**
* send command to domain
*/
send(countDomain.command.IncreaseCountCommand())
}
const handleDecrement = () => {
/**
* send command to domain
*/
send(countDomain.command.DecreaseCountCommand())
}
const handleStartIncrease = () => {
/**
* send command to domain
*/
send(countDomain.command.StartCommand('increment'))
}
const handleStartDecrease = () => {
/**
* send command to domain
*/
send(countDomain.command.StartCommand('decrement'))
}
const handleStop = () => {
/**
* send command to domain
*/
send(countDomain.command.StopCommand())
}
/**
* listen to the domain event via useRemeshEvent
*/
useRemeshEvent(countDomain.event.CountChangedEvent, (count) => {
console.log(count)
})
return (
<div id="container" style=>
<h1 id="count">{count}</h1>
<button style= onClick={handleStartIncrease}>
start increase
</button> <button style= onClick={handleIncrement}>
+1
</button> <button style= onClick={handleStop}>
stop
</button> <button style= onClick={handleDecrement}>
-1
</button> <button style= onClick={handleStartDecrease}>
start decrease
</button>{' '}
</div>
)
}
const rootElement = document.getElementById('root')
const root = createRoot(rootElement)
root.render(
<StrictMode>
<RemeshRoot>
<Counter />
</RemeshRoot>
</StrictMode>,
)
remesh v4.0 开始支持了 esm 模块输出,在现阶段可能会引起你的项目里 esm/cjs 相互依赖导致的错误。解决方式是通过 bundler 配置,关闭 esm 模块或者映射为相应的 cjs 模块。
比如,在 webpack 中,可以通过 resolve.alias 配置别名:
cjs-only
: 将 remesh
映射到 remesh/cjs
,将 remesh-*
映射到 remesh-*/cjs
esm-only
: 将 remesh
映射到 remesh/esm
,将 remesh-*
映射到 remesh-*/esm
。vite 中亦有类似的配置 resolve.alias