@3liv/rijs 中文文档教程

发布于 6年前 浏览 27 项目主页 更新于 3年前

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 dynamic import() (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

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文