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 2020