@3liv/rijs 中文文档教程
Ripple Fullstack
在服务器上:
index.js
const ripple = require('rijs')({ dir: __dirname })
在客户端上:
pages/index.html
<script src="/ripple.js"></script>
运行它:
$ node index.js
这将启动随机端口上的服务器并静态地为您的 /pages
目录提供服务。 您还可以指定要始终使用的端口
,或传递现有的HTTP 服务器
(例如来自express)。
然后客户端将只流式传输他们正在使用的细粒度资源(即所有内容都是延迟加载的,没有捆绑,没有过度获取)。
Ripple 通过在后台复制不可变的操作日志来保持客户端/服务器同步,随后在更新本地存储时更新视图或其他模块。
就是这样! 不需要样板文件,没有构建管道,没有特殊的编译,没有神奇的 CLI。
基本的 API 是:
ripple(name) // getter
ripple(name, body) // setter
ripple.on('change', (name, change) => { .. })
Components
让我们向页面添加一个 (Web) 组件:
index.html
<script src="/ripple.js"></script>
+ <my-app></my-app>
让我们定义组件:
resources/my-app.js:
export default () => ()
Ripple 与您编写组件的方式无关,它们应该是幂等的:单个渲染函数。
这很好:
resources/my-app.js:
export default (node, data) => node.innerHTML = 'Hello World!'
或者使用一些 DOM-diff 助手:
resources/my-app.js:
export default (node, data) => jsx(node)`<h1>Hello World</h1>`
或者使用一次/D3加入:
resources/my-app.js :
export default (node, data) => {
once(node)
('h1', 1)
.text('Hello World')
})
有关编写幂等组件的更多信息,请参阅此规范。
State/Data
组件的第一个参数是要更新的节点。
第二个参数包含组件需要呈现的所有状态和数据:
export default function component(node, data){ ... }
您可以通过将资源名称添加到数据属性来注入数据资源:
<my-shop data="stock">
export default function shop({ stock }){ ... }
声明组件所需的数据用于在数据更改时响应地重新呈现它。
或者,您可以直接使用
ripple.pull
来检索资源,它与 dynamic < code>import()(即从本地缓存解析或返回单个 promise):const dependency = await pull('dependency')
另一种选择是将数据从父级显式传递到组件:
once(node) ('my-shop', { stock })
辅助函数将设置状态和重绘,所以重绘父对象会重绘它的子对象。 如果您想自己做:
element.state = { stock } element.draw()
Defaults
您可以使用 ES6 语法设置默认值:
export default function shop({ stock = [] }){ ... }
如果您需要在组件的状态对象上保留默认值,您可以使用一个小的辅助函数:
export default function shop(state){
const stock = defaults(state, 'stock', [])
}
Updates
Local state
每当您需要更新本地状态时,只需更改 state
并调用重绘(如游戏循环):
export default function abacus(node, state){
const o = once(node)
, { counter = 0 } = state
o('span', 1)
.text(counter)
o('button', 1)
.text('increment')
.on('click.increment' d => {
state.counter++
o.draw()
})
}
Global state
无论何时您需要更新全局状态,您都可以简单地计算新值并再次注册它将触发更新:
ripple('stock', {
apples: 10
, oranges: 20
, pomegranates: 30
})
或者如果您只想更改资源的一部分,请使用功能运算符来应用更细粒度的差异并触发更新:
update('pomegranates', 20)(ripple('stock'))
// same as: set({ type: 'update', key: 'pomegranate', value: 20 })(ripple('stock'))
使用原子差异的日志将不变性的优势与更明智的方式在分布式环境中同步状态相结合。
默认情况下,组件是 rAF 批处理的。 您可以通过 node.changes
访问组件中自上次渲染以来所有相关更改的列表,以便在必要时提高性能。
Events
在根元素上调度一个事件以将更改传达给父元素 (node.dispatchEvent
)。
Routing
路由由您的顶级组件处理:只需解析 URL 以确定要渲染的子项并在路由更改时调用应用程序的重绘:
export function app(node, data){
const o = once(node)
, { pathname } = location
o('page-dashboard', pathname == '/dashboard')
o('page-login', pathname == '/login')
once(window)
.on('popstate.nav', d => o.draw())
}
此解决方案不绑定任何库,您可能根本不需要一个.
对于高级用例,请查看 decouter。
Styling
您可以使用 Web 组件语法(:host
等)编写您的样式表,假设它们是完全隔离的。
它们要么被插入到元素的影子根中,要么在没有影子的情况下限定范围并添加到头部。
默认情况下,CSS 资源 component-name.css
将自动应用于组件 component-name
。
但是您也可以将多个样式表应用于一个组件:只需扩展 css
属性即可。
Folder Convention
/resources
文件夹中的所有文件都将自动注册(测试等除外)。 您可以随意组织它,但我建议使用约定:每个组件一个文件夹(将 JS、CSS 和测试放在一起),以及一个 data
文件夹,用于存放构成您的资源的资源域模型。
resources
├── data
│ ├── stock.js
│ ├── order.js
│ └── ...
├── my-app
│ ├── my-app.js
│ ├── my-app.css
│ └── test.js
├── another-component
│ ├── another-component.js
│ ├── another-component.css
│ └── test.js
└── ...
开箱即用的热重载。 对这些文件的任何更改都会立即反映到各处。
Loading Resources
您还可以自己强制获取/设置资源:
ripple(name) // getter
ripple(name, body) // setter
或者例如从其他包中导入资源:
ripple
.resource(require('external-module-1'))
.resource(require('external-module-2'))
.resource(require('external-module-3'))
您还可以创建代理到 fero 的资源) 服务也。
Offline
资源当前缓存在 localStorage
中。
这意味着即使任何网络交互之前,您的应用程序也会为超快启动呈现最后已知的良好状态。
然后随着资源的流入,应用程序的相关部分也会更新。
注意:资源缓存将很快通过在引擎盖下使用 ServiceWorkers 来改进 (#27)
Render Middleware
默认情况下,draw 函数只调用元素上的函数。 您可以使用显式装饰器模式在没有任何框架挂钩的情况下扩展它:
// in component
export default function component(node, data){
middleware(node, data)
}
// around component
export default middleware(function component(node, data){
})
// for all components
ripple.draw = middleware(ripple.draw)
此构建中包含的一些有用的中间件是:
Needs
这个中间件< /a> 读取 needs
标头并将属性应用于元素。 在所有依赖项都可用之前,组件不会呈现。 当组件需要定义自己的依赖项时,这很有用。 您还可以提供一个函数来动态计算所需的资源。
export default {
name: 'my-component'
, body: function(){}
, headers: { needs: '[css=..][data=..]' }
}
Shadow
如果浏览器支持,将为每个组件创建影子根。 该组件将渲染到阴影 DOM 而不是光 DOM。
Perf (Optional)
默认情况下不包括这个,但是您可以使用它来注销每个组件呈现所花费的时间。
其他调试技巧:
检查
ripple.resources
以获得您的应用程序的快照。 资源采用元组格式{ name, body, headers }
。检查元素上的
$0.state
以查看上次渲染或操作它的状态对象。
Sync
您可以在资源标头中定义一个 from
函数,它将处理来自客户端的请求:
const from = (req, res) =>
req.data.type == 'REGISTER' ? register(req, res)
: req.data.type == 'FORGOT' ? forgot(req, res)
: req.data.type == 'LOGOUT' ? logout(req, res)
: req.data.type == 'RESET' ? reset(req, res)
: req.data.type == 'LOGIN' ? login(req, res)
: false
module.exports = {
name: 'users'
, body: {}
, headers: { from }
}
这可以返回单个值、承诺或流。 在客户端,您使用 ripple.send(name, type, value)
发出请求。 这将返回一个可等待的 stream。
您还可以使用 .subscribe
API 来订阅全部或部分资源。 键可以任意深,多个键将合并为一个对象。
ripple.subscribe(name, key)
ripple.subscribe(name, [keys])
订阅会自动进行重复数据删除和引用计数,因此组件可以独立订阅他们需要的数据,而不必担心这一点。
请注意,如果您只想获取单个值然后自动取消订阅,也可以使用 ripple.get
而不是订阅。
Ripple Minimal
如果您的前端没有后端,请查看 rijs/minimal,这是 Ripple 的客户端构建。
您还可以通过添加/删除模块来调整自己的框架。
Docs
请参阅 rijs/docs 以获取更多指南、模块索引、API 参考等
Ripple Fullstack
On the server:
index.js
const ripple = require('rijs')({ dir: __dirname })
On the client:
pages/index.html
<script src="/ripple.js"></script>
Run it:
$ node index.js
This starts up a server on a random port and statically serves your /pages
directory. You can also specify a port
to always use, or pass an existing HTTP server
(e.g. from express).
Clients will then just be streamed the fine-grained resources they are using (i.e. everything is lazy loaded, no bundling, no over-fetching).
Ripple keeps clients/servers in sync by replicating an immutable log of actions in the background, and subsequently the view - or other modules - which are reactively updated when the local store is updated.
That's it! No boilerplate necessary, no build pipeline, no special transpilation, no magical CLI.
The basic API is:
ripple(name) // getter
ripple(name, body) // setter
ripple.on('change', (name, change) => { .. })
Components
Let's add a (Web) Component to the page:
index.html
<script src="/ripple.js"></script>
+ <my-app></my-app>
Let's define the component:
resources/my-app.js:
export default () => ()
Ripple is agnostic to how you write your components, they should just be idempotent: a single render function.
This is fine:
resources/my-app.js:
export default (node, data) => node.innerHTML = 'Hello World!'
Or using some DOM-diff helper:
resources/my-app.js:
export default (node, data) => jsx(node)`<h1>Hello World</h1>`
Or using once/D3 joins:
resources/my-app.js:
export default (node, data) => {
once(node)
('h1', 1)
.text('Hello World')
})
For more info about writing idempotent components, see this spec.
State/Data
The first parameter of the component is the node to update.
The second parameter contains all the state and data the component needs to render:
export default function component(node, data){ ... }
You can inject data resources by adding the name of the resources to the data attribute:
<my-shop data="stock">
export default function shop({ stock }){ ... }
Declaring the data needed on a component is used to reactively rerender it when the data changes.
Alternatively, you can use
ripple.pull
directly to retrieve a resource, which has similar semantics to dynamicimport()
(i.e. resolves from local cache or returns a single promise):const dependency = await pull('dependency')
The other option is to explicitly pass down data to the component from the parent:
once(node) ('my-shop', { stock })
The helper function will set the state and redraw, so redrawing a parent will redraw it's children. If you want to do it yourself:
element.state = { stock } element.draw()
Defaults
You can set defaults using the ES6 syntax:
export default function shop({ stock = [] }){ ... }
If you need to persist defaults on the component's state object, you can use a small helper function:
export default function shop(state){
const stock = defaults(state, 'stock', [])
}
Updates
Local state
Whenever you need to update local state, just change the state
and invoke a redraw (like a game loop):
export default function abacus(node, state){
const o = once(node)
, { counter = 0 } = state
o('span', 1)
.text(counter)
o('button', 1)
.text('increment')
.on('click.increment' d => {
state.counter++
o.draw()
})
}
Global state
Whenever you need to update global state, you can simply compute the new value and register it again which will trigger an update:
ripple('stock', {
apples: 10
, oranges: 20
, pomegranates: 30
})
Or if you just want to change a part of the resource, use a functional operator to apply a finer-grained diff and trigger an update:
update('pomegranates', 20)(ripple('stock'))
// same as: set({ type: 'update', key: 'pomegranate', value: 20 })(ripple('stock'))
Using logs of atomic diffs combines the benefits of immutability with a saner way to synchronise state across a distributed environment.
Components are rAF batched by default. You can access the list of all relevant changes since the last render in your component via node.changes
to make it more performant if necessary.
Events
Dispatch an event on the root element to communicate changes to parents (node.dispatchEvent
).
Routing
Routing is handled by your top-level component: Simply parse the URL to determine what children to render and invoke a redraw of your application when the route has changed:
export function app(node, data){
const o = once(node)
, { pathname } = location
o('page-dashboard', pathname == '/dashboard')
o('page-login', pathname == '/login')
once(window)
.on('popstate.nav', d => o.draw())
}
This solution is not tied to any library, and you may not need one at all.
For advanced uses cases, checkout decouter.
Styling
You can author your stylesheets assuming they are completely isolated, using the Web Component syntax (:host
etc).
They will either be inserted in the shadow root of the element, or scoped and added to the head if there is no shadow.
By default, the CSS resource component-name.css
will be automatically applied to the component component-name
.
But you can apply multiple stylesheets to a component too: just extend the css
attribute.
Folder Convention
All files in your /resources
folder will be automatically registered (except tests etc). You can organise it as you like, but I recommend using the convention: a folder for each component (to co-locate JS, CSS and tests), and a data
folder for the resources that make up your domain model.
resources
├── data
│ ├── stock.js
│ ├── order.js
│ └── ...
├── my-app
│ ├── my-app.js
│ ├── my-app.css
│ └── test.js
├── another-component
│ ├── another-component.js
│ ├── another-component.css
│ └── test.js
└── ...
Hot reloading works out of the box. Any changes to these files will be instantly reflected everywhere.
Loading Resources
You can also get/set resources yourselves imperatively:
ripple(name) // getter
ripple(name, body) // setter
Or for example import resources from other packages:
ripple
.resource(require('external-module-1'))
.resource(require('external-module-2'))
.resource(require('external-module-3'))
You can also create resources that proxy to fero) services too.
Offline
Resources are currently cached in localStorage
.
This means even before any network interaction, your application renders the last-known-good-state for a superfast startup.
Then as resources are streamed in, the relevant parts of the application are updated.
Note: Caching of resources will be improved by using ServiceWorkers under the hood instead soon (#27)
Render Middleware
By default the draw function just invokes the function on an element. You can extend this without any framework hooks using the explicit decorator pattern:
// in component
export default function component(node, data){
middleware(node, data)
}
// around component
export default middleware(function component(node, data){
})
// for all components
ripple.draw = middleware(ripple.draw)
A few useful middleware included in this build are:
Needs
This middleware reads the needs
header and applies the attributes onto the element. The component does not render until all dependencies are available. This is useful when a component needs to define its own dependencies. You can also supply a function to dynamically calculate the required resources.
export default {
name: 'my-component'
, body: function(){}
, headers: { needs: '[css=..][data=..]' }
}
Shadow
If supported by the browser, a shadow root will be created for each component. The component will render into the shadow DOM rather than the light DOM.
Perf (Optional)
This one is not included by default, but you can use this to log out the time each component takes to render.
Other debugging tips:
Check
ripple.resources
for a snapshot of your application. Resources are in the tuple format{ name, body, headers }
.Check
$0.state
on an element to see the state object it was last rendered with or manipulate it.
Sync
You can define a from
function in the resource headers which will process requests from the client:
const from = (req, res) =>
req.data.type == 'REGISTER' ? register(req, res)
: req.data.type == 'FORGOT' ? forgot(req, res)
: req.data.type == 'LOGOUT' ? logout(req, res)
: req.data.type == 'RESET' ? reset(req, res)
: req.data.type == 'LOGIN' ? login(req, res)
: false
module.exports = {
name: 'users'
, body: {}
, headers: { from }
}
This can return a single value, a promise or a stream. On the client you make requests with ripple.send(name, type, value)
. This returns an awaitable stream.
You can also use the .subscribe
API to subscribe to all or part of a resource. The key can be arbitrarily deep, and multiple keys will be merged into a single object.
ripple.subscribe(name, key)
ripple.subscribe(name, [keys])
Subscriptions are automatically deduplicated are ref-counted, so components can indepedently subscribe to the data they need without worrying about this.
Note that you can also use ripple.get
instead of subscribe if you just want to get a single value and then automatically unsubscribe.
Ripple Minimal
If you have don't have backend for your frontend, checkout rijs/minimal which is a client-side only build of Ripple.
You can also adjust your own framework by adding/removing modules.
Docs
See rijs/docs for more guides, index of modules, API reference, etc
你可能也喜欢
- @1024-fontend-group/el-form-renderer 中文文档教程
- @1ziton/react-native-scancode 中文文档教程
- @2toad/diff-match-patch 中文文档教程
- @4dsas/doc_preprocessing 中文文档教程
- @4th-motion/stylelint-config 中文文档教程
- @5no/i18n 中文文档教程
- @75lb/ghci 中文文档教程
- @7speck/logger 中文文档教程
- @_lukepatrick/postgraphile-upsert-plugin 中文文档教程
- @aacassandra/client-xlsx2json 中文文档教程