OneFx is a full-stack framework for building web apps. Here are the features youβll find in Onefx.js:
Server-side rendering and universal rendering with React and Redux Apollo GraphQL (docs + playground), ES2017, TypeScript, TSX support out of the box Server-side development via Koa.js Create a project# Copy git clone https://github.com/puncsky/web-onefx-boilerplate.git my-awesome-project
Run your project# This is intended for *nix users. If you use Windows, go to Run on Windows . Letβs first prepare the environment.
Copy cd my-awesome-project
nvm use 10.15 .0
npm install
cp ./.env.tmpl ./.env
Development mode# To run your project in development mode, run:
The development site will be available at http://localhost:5000 .
Production Mode# Itβs sometimes useful to run a project in production mode, for example, to check bundle size or to debug a production-only issue. To run your project in production mode locally, run:
Copy npm run build-production
NODE_ENV = production npm run start
NPM scripts# npm run test
: test the whole project and generate a test coveragenpm run ava ./path/to/test-file.js
: run a specific test filenpm run build
: build source code from src
to dist
npm run lint
: run the linternpm run flow
: run the flow type checknpm run kill
: kill the node server occupying the port 4100. Code Styles# We use prettier, tslint, and editorconfig to enforce consistent styles across the whole project, so that we will not bikeshed on coding styles in the code review.
However, please visit our Contributing Code before submitting your code.
Architecture#
Copy .
βββ README.md
βββ ava.config.js // ava test util configuration
βββ babel.config.js // babel compiler/transpiler configuration
βββ babel-register.js. // babel register options
βββ config // project configuration
βΒ Β βββ default.js // base config to be extended in all env
βΒ Β βββ development.js // config in NODE_ENV=development
βΒ Β βββ production.js // config in NODE_ENV=production
βΒ Β βββ test.js // config in NODE_ENV=test
βββ coverage // test coverage
βββ dist // destination for src build result
βββ gulpfile.babel.js // gulp task runner config
βββ package.json
βββ renovate.json // renovate bot to automate dependency bumps
βββ server.ts // project entry
βββ src // source code
βΒ Β βββ api-gateway // APIs server defined in GraphQL for the clients to call
βΒ Β βΒ Β βββ api-gateway.graphql
βΒ Β βΒ Β βββ api-gateway.ts
βΒ Β βΒ Β βββ resolvers
βΒ Β βΒ Β βββ meta-resolver.ts
βΒ Β βββ client // browser-side source code
βΒ Β βΒ Β βββ javascripts
βΒ Β βΒ Β βΒ Β βββ main.tsx
βΒ Β βΒ Β βββ static
βΒ Β βΒ Β βΒ Β βββ favicon.png
βΒ Β βΒ Β βΒ Β βββ manifest.json
βΒ Β βΒ Β βΒ Β βββ robots.txt
βΒ Β βΒ Β βββ stylesheets
βΒ Β βΒ Β βββ main.scss
βΒ Β βββ model // data models
βΒ Β βΒ Β βββ index.ts
βΒ Β βΒ Β βββ model.ts
βΒ Β βββ server // onefx server
βΒ Β βΒ Β βββ index.ts
βΒ Β βΒ Β βββ middleware // koa middleware
βΒ Β βΒ Β βΒ Β βββ index.ts
βΒ Β βΒ Β βΒ Β βββ manifest-middleware.ts
βΒ Β βΒ Β βΒ Β βββ set-middleware.ts
βΒ Β βΒ Β βββ server-routes.tsx // server-side routes
βΒ Β βΒ Β βββ start-server.tsx // server initialization
βΒ Β βββ shared // js code shared by both the server and the client
βΒ Β βββ app-container.ts
βΒ Β βββ app.ts
βΒ Β βββ common
βΒ Β βββ home
βΒ Β βΒ Β βββ home.ts
βΒ Β βββ register-service-worker.js
βββ translations // translations supported in this website
βΒ Β βββ en.yaml
βΒ Β βββ zh-cn.yaml
βββ Procfile // heroku Procfile
βββ webpack.js // webpack bundler config
Guides# State management# We use redux to manage state in onefx.js. To pass the state from the server to the initial page during the server-side rendering, in the server use ctx.setState(path, val)
to set the state:
Copy server . get ( 'SPA' , '/*' , function onRoute ( ctx ) {
ctx . setState ( 'base.sampleState' , 'this is a sample initial state' ) ;
ctx . body = ctx . isoReactRender ( {
VDom : (
< AppContainer / >
) ,
reducer : noopReducer ,
clientScript : '/main.js' ,
} ) ;
} ) ;
And use the state in the react component:
Copy const SampleStateContainer = connect (
state => ( { text : state . base . sampleState } )
) ( function SampleState ( { text } ) {
return (
< div > { text } < / div >
) ;
} ) ;
Styling# We support both global styles with sass in ./src/client/stylesheets/main.scss
and modular styles with styletron-react :
Copy import react from 'react' ;
import { styled } from 'onefx/lib/styletron-react' ;
const Panel = styled ( 'div' , {
backgroundColor : 'silver' ,
} ) ;
export default < Panel > Hello < / Panel > ;
Routing# server-side routing is using koa-router and located in ./src/server/server-routes.js
. The basic usage is:
Copy server
. get ( '/' , ( ctx , next ) => {
ctx . body = 'Hello World!' ;
} )
. post ( '/users' , ( ctx , next ) => {
} )
. put ( '/users/:id' , ( ctx , next ) => {
} )
. del ( '/users/:id' , ( ctx , next ) => {
} )
. all ( '/users/:id' , ( ctx , next ) => {
} ) ;
client-side routing is using react-router v4 and located in ./src/shared/app.js
.
Copy < Switch >
< Route exact path = '/' component = { Home } / >
< Route component = { NotFound } / >
< / Switch >
Fetching data# We use Apollo Graphql and TypeGraphQL for universal rendering with React. For detailed documentation, please visit:
See "+1" example in the boilerplate Define GraphQL schemas in TypeScript Learn how to fetch data with the Apollo Query component Make a query# In src/api-gateway/resolvers/
, define a new resolver and method. Take the meta data endpoint of the server health for example.
Copy import { Query , Resolver , ResolverInterface } from "type-graphql" ;
@ Resolver ( _ => String )
export class MetaResolver implements ResolverInterface < ( ) => String > {
@ Query ( _ => String , { description : "is the server healthy?" } )
public async health ( ) : Promise < string > {
return "OK" ;
}
}
and then in api-gateway.ts
, mount the resolver.
Copy const resolvers = [ MetaResolver ] ;
Now the server is ready and you can call the health
endpoint at https://localhost:5000/api-gateway/ .
The next step is to call it from the React component.
Organizing components with data# Directory structure
Copy .
βββ components
βΒ Β βββ health-text.tsx
βββ data
βΒ Β βββ __generated__
βΒ Β βΒ Β βββ health.ts
βΒ Β βββ queries.ts // or / and mutations.ts
βββ health-controller.tsx
βββ hooks
βΒ Β βββ use-health.ts
βββ index.ts
components
is for view components only, for example health-text.tsx
Copy import { colors } from "@/shared/common/styles/style-color" ;
import CheckCircleTwoTone from "@ant-design/icons/CheckCircleTwoTone" ;
import CloseCircleTwoTone from "@ant-design/icons/CloseCircleTwoTone" ;
import LoadingOutlined from "@ant-design/icons/LoadingOutlined" ;
import React from "react" ;
type Props = {
loading : boolean ;
health ? : string ;
error : boolean ;
} ;
export const HealthText : React . FC < Props > = ( {
loading ,
health ,
error
} : Props ) => {
if ( loading ) {
return (
< div >
< LoadingOutlined /> Checking Status
</ div >
) ;
}
if ( error ) {
return (
< div >
< CloseCircleTwoTone twoToneColor = { colors . error } /> Not OK
</ div >
) ;
}
return (
< div >
< CheckCircleTwoTone twoToneColor = { colors . success } /> { health }
</ div >
) ;
} ;
data
is for GraphQL queries and mutations. For example, health-controller.tsx
. Whenever it changes, please run npm run schema:generate
to generate type definitions again.
Copy import gql from "graphql-tag" ;
export const getHealth = gql `
query GetHealth {
health
}
` ;
hooks
are lifecyle functions for components
Copy import { GetHealth } from "@/shared/home/data/__generated__/getHealth" ;
import { getHealth } from "@/shared/home/data/quries" ;
import { useQuery } from "react-apollo" ;
export const useGetHealth = ( ) => {
const { loading , data , error , refetch } = useQuery < GetHealth > ( getHealth , {
ssr : false
} ) ;
return { loading , data , error , refetch } ;
} ;
And finally *-controller.tsx
connects data with view components via hooks.
Copy import { useGetHealth } from "@/shared/home/hooks/use-health" ;
import React from "react" ;
import { HealthText } from "./components/health-text" ;
export const HealthController : React . FC = ( ) => {
const { loading , data , error } = useGetHealth ( ) ;
return < HealthText loading = { loading } error = { ! ! error } health = { data ? . health } /> ;
} ;
Internationalization# Onefx reads translations from ./translations
directory. Please create a file there named with a corresponding locale, for example, en.yaml
. And then add an entry
Copy homepage.hello : hello , $ { userName } !
React / Client-side# and then in the react view file (client-side)
Copy import { t } from 'onefx/lib/iso-i18n' ;
function Greetings ( ) {
return (
< div > { t ( 'homepage.hello' , { userName : 'John' } ) } < / div >
) ;
}
When users visit this site with accept-language: en
in the header, which is set by the browser, then they will see translated greetings. If you want to explicitly set the locale, then visit the page with a query string ?locale=en
then it will memorize this in the cookie.
Server-side# t
singleton function does not work in the server-side because the async calls may switch the context and mix it with requests from other languages. In this case, please use ctx.t
instead.
Testing# test files are supposed to be placed in any module like ./__test__/example.test.js
in ava test utils .
Copy import test from 'ava' ;
test ( 'testname' , async t => {
} ) ;
Security# Onefx enables secure web app development with
CSRF protection that can be exempted at ./config/default.js
(config.server.noCsrfRoutes
) Helmet headers that can be exempted at config.server.noSecurityHeadersRoutes
Content Security Policy configured at config.csp
for example, in default.js
,
Copy server : {
noSecurityHeadersRoutes : {
'/embed/checkout/' : true ,
} ,
noCsrfRoutes : {
'/api-gateway/' : true ,
} ,
} ,
csp : {
'default-src' : [
'none' ,
] ,
}
Static assets# Static assets are placed in ./client/static/
and loaded into the root directory of the website. Take ./client/static/favicon.png
for example, you can get it at http://localhost:4100/favicon.png , or use it in the react component:
Copy import { assetURL } from 'onefx/lib/asset-url' ;
function ImgExample ( ) {
return (
< img src = { assetURL ( 'favicon.png' ) } / >
) ;
}
Configuration# Environment variables# The environment variable is read from commandline as well as .env
file. Take PORT
for example,
or in .env
file
In the js file, you can read the value by process.env.PORT
.
Static configuration# The static configuration is located in ./config
and can be read according to the environment variable NODE_ENV
.
CDN# If you want to setup CDN for the static resources, I recommend BunnyCDN for its ease-of-use and cost-effectiveness. Then configure OneFx as
Copy module . exports = {
server : {
cdnBase : 'https://example-cdn.net' ,
}
}
And then when loading static assets, you just follow the same practice with the static assets.
Copy import { assetURL } from 'onefx/lib/asset-url' ;
function ImgExample ( ) {
return (
< img src = { assetURL ( 'favicon.png' ) } / >
) ;
}
References# Tech Stack
Design Resources
Run on Windows# install Windows Subsystem for Linux . Choose Ubuntu, for example. On WSL Ubuntu, install node version manager and install the latest lts dubnium Copy curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash
export NVM_DIR = " $HOME /.nvm"
[ -s " $NVM_DIR /nvm.sh" ] && \ . " $NVM_DIR /nvm.sh"
[ -s " $NVM_DIR /bash_completion" ] && \ . " $NVM_DIR /bash_completion"
nvm ls
nvm install lts/Dubnium
nvm use lts/dubnium
clone repo to C:/
Copy cd /mnt/c/
git clone https://github.com/puncsky/web-onefx-boilerplate.git
install VS Code and open WSL terminal with ctrl + ` . Not sure about WSL terminal? Go to this post .