10 interesting things in Nautil.js

Nautil is a javascript framework based on React which is a modern reactive UI library. In the ecosystem of react, developers are always following Flux architecture.

However, its not easy to write application level code with react. Even though we have redux and many third libraries, we still should have to waste much time on resolving code organization.

To make it easy to use react syntax to create applications, I wrote a js framework which called Nautil. It is much difference from native react development.

Now, follow me to have a glance of what Nautil provides.

1. Observer

The whole framework is built on the idea of Observer Pattern. This help developers write less code to implement reactive system. For example:

import { Component, Store } from 'nautil'
import { Observer, Text } from 'nautil/components'

const store = new Store({ age: 10 })

class SomeComponent extends Component {
  render() {
    return (
      <Observer
        subscribe={dispatch => store.watch('age', dispatch)}
        unsubscribe={dispatch => store.unwatch('age', dispatch)}
        dispatch={this.update}
      >
        <Text>{store.state.age}</Text>
      </Observer>
    )
  }
}
// in some place, even outside the file by exporting `store`
store.state.age = 20

Here we use a Observer component to wrap sub-components, and when its dispatch is invoked, the component will rerender. By using Observer component, we can write reactive code more interesting, any responsive object can be used in react.

2. Store

It is too complex by using redux, why should we write so many codes which is not about our business? Nautil provides a inner Store which is very easy to define, and use like vue data.

import { Store } from 'nautil'

const store = new Store({
  name: 'tomy',
  age: 10,
})

Use api to get and set data:

const name = store.get('name')
store.set('name', 'sunny')

However, to more sense way is to use state:

const { state } = store
const name = state.name
state.name = 'sunny'

To worker with Observer, store can be watched so that rerender the UI when data changed.

const WatchedComponent = observe(store)(OriginComponent)

The WatchedComponent is reactive of store, so when the data changed in store, it will rerender UI.

3. Two-Way-Binding

With the ability of Observer, I build up a two-way-binding system. Yes, you can use two-way-binding in react too.

import { Component } from 'nautil'
import { Input } from 'nautil/components'

class EditComponent extends Component {
  state = {
    name: '',
  }
  render() {
    return (
      <Input $value={[this.state.name, name => this.setState({ name })]} />
    )
  }
}

The property $value which begin with $ is a two-way-binding property. It receive an array which contains two item. The second item is a function which is to update the value.

By using createTwoWayBinding and Store, it very easy to write beautiful codes.

import { Component, Store } from 'nautil'
import { Input } from 'nautil/components'
import { inject, observe, pipe } from 'nautil/operators'

class EditComponent extends Component {
  render() {
    return (
      <Input $value={this.attrs.binding.name} />
    )
  }
}

const store = new Store({ name: '' })
const binding = createTwoWayBinding(store.state)

export default pipe([
  inject('binding', binding),
  observe(store),
])(EditComponent)

We use createTwoWayBinding to create a proxied object. When we invoke state.name, we will get a structured array.

And it is very easy and interesting to use two-way-binding property inside component. If I want to create a component like following:

<Swither $open={binding.open} />

We can easily write in the component:

class Swither extends Component {
  onToggle() {
    this.attrs.open = !this.attrs.open
  }
}

I do not need to write a lot of callback functions, just change the this.attrs.open. Isn't it interesting?

4. operators

If you have used react-redux, you will know how to use connect function to wrap a component. In Nautil, operators are functions to create wrap function.

In Nautil, operators are much more powerful than redux connect.

- observe: short for Observer
- inject: pend a new prop
- connect: inject ReactConext into a prop
- pollute: change sub-components' defaultProps in runtime of current component
- scrawl: change sub-components' defaultStylesheet in runtime

- pipe: combine operators
- multiple: use batch operator parameters one time

Especially in an application, we would want to bypass some props, well, pollute operator is a magic. For example, you want to inject some component with an object globally:

class App extends Component {
  render() {
    ...
  }
}

const pollutedProps = { store }
export default pipe([
  multiple(pollute, [
    [ComponentA, pollutedProps],
    [ComponentB, pollutedProps],
    [ComponentC, pollutedProps],
  ]),
  observe(store),
])(App)

Using the previous code, your App will be reactive for store and the given sub-deep-components inside App will auto be patched with store prop.

5. Depository

To request data from backend, yep, use ajax. But in fact, we do not need to write ajax code in your project. Depository is the one to help you throw away ajax.

It is an abstract of data request, you need to know one core concepts: data source. A data source is a configuration for data request, and use the id to get data from depository without ajax code.

import { Depository } from 'nautil'

const depo = new Depository({
  name: 'depo_name',
  baseURL: '/api/v2',
  sources: [
    {
      id: 'some',
      path: '/some',
      method: 'get',
    },
  ],
})

I defined a data source 'some' in the depository 'depo_name', and then I can request the data by:

const data = depo.get('some') // get data from depo cache
depo.request('some').then(data => console.log(data)) // request data from backend in a Promise

.get is different from .request, it do not request data from backend immediately, it request data from local cache first, so it is synchronous. Working with observe:

class SomeComponent extends Component {
  render() {
    const { depo } = this.attrs
    const some = depo.get('some')
    return (
      <Prepare isReady={some} loading={<Text>loading...</Text>}>
        {() => <Text>{some.name}</Text>}
      </Prepare>
    )
  }
}

export default pipe([
  inject('depo', depo),
  observe(dispatch => depo.subscribe('some', dispatch), dispatch => depo.unsubscribe('some', dispatch)),
])(SomeComponent)

You do not need to send ajax in this code, depository will do it for you inside. Because of subscribe to depo, the UI will rereender automatically.

6. Stylesheet

Nautil component will parse stylesheet automatically to be used in different platform.

<Section stylesheet={'className'}></Section>  ## string
<Section stylesheet={{ className: this.state.name === 'tomy' }}></Section> ## object with boolean value
<Section stylesheet={{ color: 'red', width: 120, height: 90 }}></Section> ## style object in react
<Section stylesheet={['className', { otherClass: this.state.boolean }, { color: 'blue', fontSize: 14 }]}></Section> ## mix array

Especially, when you set transform style, you do not need to worry about react-native parsing, Nautil will do it automatically.

<Section stylesheet={{ transform: 'translateX(-5px)' }}></Section>

7. cross-platform

One of Nautil's goals is to build cross-platform applications. Currently, nautil support the following platforms: web, web-mobile, web-component (h5-app), react-native (ios, andriod), miniapp (wechat-app, others use antmove to transform).

I have create a CLI tool nautil-cli, which can helop developers to start their nautil application more easy.

This is the real time to Write one, Run anywhere. Clone nautil-demo for playing.

8. Stream

Different from react event system, Nauitl allow developers to use rxjs in their event, the events handler functions can be normal handler function to receive callback parameters. Or it can be observable stream pipe operators.

<SomeComponent onHint={[map(e => e.target.value * 2), value => this.setState({ value })]}></SomeComponent>

In the previous code, the first item is a rxjs pipe operator, and the latest item in the array is onHint callback function which receive the stream output.

In the component, developers can use this.onHint$ to operate onHint event stream.

class SomeComponent extends Component {
  onDigested() {
    this.onHint$.subscribe((value) => {
      // you can subscribe on the stream when digested
      // so that, you do not need to write a wrapper handle
    })
  }
  handle(e) {
    this.onHint$.next(e)
  }
}

9. Model

Modern frontend applications are always struggling with data. Nautil provide a Model to control data for some necessary place, for example, in a form.

Model is a very strong data type controller, which is based on a Schema system.

import { Model } from 'nautil'
import { Natural } from 'nautil/types'

class PersonModel extends Model {
  schema() {
    return {
      name: {
        type: String,
        default: '',
        validators: [
          {
            validate: value => value && value.length > 6,
            message: 'name should must longer than 6 letters.',
          },
        ],
      },
      age: {
        type: Natural,
        default: 0,
        get: value => value + '', // convert to be a string when get
        set: value => +value, // convert to be a number when save into model
      },
    }
  }
}
const model = new PersonModel() // you can set default value here
const state = model.state // the same usage as Store

The model instance is very sensitive with data type. When you set a un-checked value into it, it may not accept the value because of data checking fail.

On the other hand, the validators formulators are very useful in form, for example validate in runtime:

<Section><Input $value={[state.name, name => state.name = name]} /></Section>
{model.message('name') ? <Section stylesheet="error-message">{model.message('name')}</Section> : null}

And Model instance is observable too, so you can use it with observe operator in your component.

export default pipe([
  initialize('person', PersonModel),
  observe('person'),
])(SomeComponent)

Read more from my blog to taste Model.

10. Props Statement

Although you can use prop-types to check data type in react, Nautil provides a more sensitive type checking system based on tyshemo, which can check deep nested object easily.

class SomeComponent extends Component {
  static props = {
    source: {
      name: String,
      books: [
        {
          name: String, 
          price: Positive,
        },
      ],
    },
  }
}

It is very intuitive, without any understanding. However, it is compatible with prop-types, so that all react components can be used in Nautil system.

These are what Nautil bring up which are different from react development. It help developers write less code and make code structure more clear. If you are tired with complex scattered react ecology libraries, have a try with Nautil.

The next step of Nautil is to make a UI framework which can run cross-platform. If you a interested in this project, welcome to join me on github.

2019-10-13 76

为价值买单

本文价值0.76RMB