henri is an easy to learn rails-like, server-side rendered (react & vue) with powerful ORMs
yarn global add henri
# or
npm install -g henri
henri new <folder name>
The above command will create a directory structure similar to this:
├── app
│ ├── controllers
│ ├── helpers
│ ├── models
│ └── views
│ ├── assets
│ ├── components
│ ├── pages
│ ├── public
│ │ ├── css
│ │ ├── fonts
│ │ ├── img
│ │ ├── js
│ │ └── patterns
│ └── styles
├── config
│ ├── default.json
│ ├── production.json
│ ├── routes.js
│ └── webpack.js <- Overload Next.js webpack settings
├── test
│ ├── controllers
│ ├── helpers
│ ├── models
│ └── views
├── package.json
If you have a Ruby on Rails background, this might look familiar.
One last step to start coding is:
cd <folder name>
henri server
And you're good to go!
The configuration is a json file located in the config
directory.
henri will try to load the file matching your NODE_ENV
and will fallback to default
.
You can have a default.json
, production.json
, etc.
{
"stores": {
"default": {
"adapter": "mongoose",
"url": "mongodb://user:pass@mongoserver.com:10914/henri-test"
},
"dev": {
"adapter": "disk"
}
},
"secret": "25bb9ed0b0c44cc3549f1a09fc082a1aa3ec91fbd4ce9a090b",
"renderer": "react"
}
You can easily add models under app/models
.
They will be autoloaded and available throughout your application (exposed globally).
We use Mongoose for MongoDB, Sequelize for SQL adapters and Waterline for the disk adapter.
You can use the command-line to generate models:
# henri g model modelname name:string! age:number notes:string birthday:date!
// app/models/User.js
// Whenever you have a User model, it will be overloaded with the following:
// email: string
// password: string
// beforeCreate: encrypts the password
// beforeUpdate: encrypts the password
module.exports = {
store: 'dev', // see the demo configuration up there
name: 'user_collection', // will use user_collection' instead of 'users'
schema: {
firstName: { type: 'string' },
lastName: String,
tasks: {},
},
};
// app/models/Tasks.js
module.exports = {
store: 'default', // see the demo configuration up there
schema: {
name: { type: 'string', required: true },
category: {
type: 'string',
validations: {
isIn: ['urgent', 'high', 'medium', 'low'],
},
defaultsTo: 'low',
},
},
};
The disk adapter is using Waterline to provide disk-based storage.
This is not for production and you can easily port your models to other adapters.
yarn add @usehenri/disk
# or
npm install @usehenri/disk --save
The MongoDB adapter is using Mongoose to provide a MongoDB ODM.
yarn add @usehenri/mongoose
# or
npm install @usehenri/mongoose --save
The MySQL adapter is using Sequelize to provide a MySQL ORM.
yarn add @usehenri/mysql
# or
npm install @usehenri/mysql --save
The MSSQL adapter is using Sequelize to provide a MSSQL ORM.
yarn add @usehenri/mssql
# or
npm install @usehenri/mssql --save
The PostgresQL adapter is also using Sequelize to provide a PostgresQL ORM.
yarn add @usehenri/postgresql
# or
npm install @usehenri/postgresql --save
You can add a graphql
key to your schema file and they will be automatically loaded, merged and available.
// app/models/Task.js
const types = require('@usehenri/mongoose/types');
module.exports = {
schema: {
description: { type: types.STRING, required: true },
type: { type: types.ObjectId, ref: 'Type', required: true },
location: { type: types.ObjectId, ref: 'Location', required: true },
reference: { type: types.STRING, required: true },
notes: { type: types.STRING },
oos: { type: types.BOOLEAN, default: false },
},
options: {
timestamps: true,
},
graphql: {
types: `
type Task {
_id: ID!
reference: String!
description: String!
location: Location
type: Type
notes: String!
oos: Boolean
}
type Query {
tasks: [Task]
task(_id: ID!): Task
}
`,
resolvers: {
Query: {
tasks: async () => {
return Task.find()
.populate('type location')
.exec();
},
task: async (_, id) => await Task.findOne(id).populate('type'),
},
},
},
};
You will be able to query this anywhere. Even as an argument to res.render()
. See below:
// app/controllers/tasks.js
// henri has a gql function which does nothing but help editors parse gql...!
const { gql } = henri;
module.exports = {
index: async (req, res) => {
return res.render('/tasks', {
graphql: gql`
{
tasks {
_id
reference
description
type {
_id
name
prefix
counter
}
location {
_id
name
}
}
locations {
_id
name
}
}
`,
});
},
};
You can use React, Vue and Handlebars as renderer. They are all server-side rendered and the first two options use webpack to push updates to the browser.
We use next.js to render pages and injectdata from controllers. You can only add pages and if the defined routes don'tmatch, and next matches a route, it will be rendered.
Usage (config file):
{
"renderer": "react"
}
Example:
// app/views/pages/log.js
import React from 'react';
import Link from 'next/link';
export default data => (
<div>
<div>{data}</div>
<Link href="/home">
<a>Home</a>
</Link>
</div>
);
You can also add webpack configuration in config/webpack.js
:
// If you want to have jQuery as a global...
module.exports = {
webpack: async (config, { dev }, webpack) => {
config.plugins.push(
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
})
);
return config;
},
};
You can use Inferno instead of React in production. In development, React will be used for hot re/loading.
Installation:
yarn add react react-dom inferno inferno-compat inferno-server
Usage (config file):
{
"renderer": "inferno"
}
You can use Preact instead of React in production. In development, React will be used for hot re/loading.
Installation:
yarn add react react-dom preact preact-compat
Usage (config file):
{
"renderer": "preact"
}
We use Nuxt.js to render pages and injectdata from controllers. You can only add pages and if the defined routes don'tmatch, and nuxt matches a route, it will be rendered.
Usage (config file):
{
"renderer": "vue"
}
Example:
<template>
<div>
<h1>Welcome!</h1>
<nuxt-link to="/about">About page</nuxt-link>
</div>
</template>
The handlebars options renders your .html
or .hbs
files under app/views/pages
.
It will also load partials from app/views/partials
Usage (config file):
{
"renderer": "template"
}
Example:
<html>
<head>
<title>Hello!</title>
</head>
<body>
{{> somePartials }}
<li>Some data: {{hello}}</li>
</body>
</html>
You can refetch data from any data-hydrated controller endpoint with GET using the application/json
header.
You can easily add controllers under app/controllers
.
They will be autoloaded and available throughout your application.
Controllers are auto-reloaded on save.
// app/controllers/User.js
module.exports = {
info: async (req, res) => {
if (!req.isAuthenticated()) {
return res.status(403).send("Sorry! You can't see that.");
}
const { user } = henri;
if (await User.count({ email: 'felix@usehenri.io' })) {
await User.update({ email: 'felix@usehenri.io' }, { password: 'blue' });
return res.send('user exists.');
}
try {
await user.compare('moo', pass);
res.send('logged in!');
} catch (error) {
res.send('not good');
}
},
create: (req, res) => {
await User.create({ email: 'felix@usehenri.io', password: 'moo' });
},
fetch: async (req, res) => {
const users = await User.find();
res.send(users);
},
postinfo: async (req, res) => {
let data = req.isAuthenticated() ? await User.find() : {};
res.render('/log', data);
}
};
Routes are defined in config/routes.js
. Also, any pages in app/views/pages
willbe rendered if no routes match before.
Routes are a simple object with a key standing as a route or an action verb(used by express) and a route.
If you want to access the res.render
data, you can make the call withapplication/json
header. Everything else will be rendered.
// config/routes.js
module.exports = {
'/test': 'user#info', // default to 'get /test'
'/abc/:id': 'moo#iii', // as this controller does not exists, route won't be loaded
'/user/find': 'user#fetch',
'get /poo': 'user#postinfo',
'post /poo': 'user#create',
'get /secured': {
controller: 'secureController#index',
roles: ['admin'],
},
'resources todo': {
controller: 'todo',
},
'crud categories': {
scope: 'api',
controller: 'categories',
omit: ['destroy'], // DELETE route will not be loaded
},
};
You can specify an array of roles which need to be matched to access the routes.
The crud
keyword (instead of http verbs) will create routes in a predefined way:
// 'crud happy': 'life'
GET /happy => life#index
POST /happy => life#create
PATCH /happy/:id => life#update
PUT /happy/:id => life#update
DELETE /happy/:id => life#destroy
The resources
keyword (instead of http verbs) add views target to CRUD, ending up with:
// 'resources happy': 'life'
GET /happy => life#index
POST /happy => life#create
PATCH /happy/:id => life#update
PUT /happy/:id => life#update
DELETE /happy/:id => life#destroy
GET /happy/:id/edit => life#edit
GET /happy/new => life#new
GET /happy/:id => life#show
You can add scope
to your routes to prefix them with anything you want.
You can add omit
array to your routes to prevent this route to be created.
We use nodemailer to provide email capabilities.
When running tests, we use nodemailer's ethereal fake-mail service.
{
"mail": {
// ...Same as nodemailer's config
}
}
We provide a wrapper around nodemailer.SendMail
:
await henri.mail.send({
from: '"Henri Server" <foo@example.com>', // sender address
to: 'bar@example.com, baz@example.com', // list of receivers
subject: 'Hello ✔', // Subject line
text: 'Hello world?', // plain text body
html: '<b>Hello world?</b>', // html body
});
If you are using the test accounts, you will see a link to your email in the console.
You can access nodemailer's package directly from henri.mail.nodemailer
andtransporter from henri.mail.transporter
.
You can add files under app/workers
and they will be auto-loaded, watched and reloaded.
If they export a start()
and a stop()
method, they will be call when initializing and tearing down (reload also).
Example:
let timer;
const start = h => {
h.pen.info('worker started');
timer = setInterval(
() => h.pen.warn(`the argument is the henri object`),
5000
);
};
const stop = () => clearInterval(timer);
module.exports = { start, stop };
Bundle the best tools in a structured environment to provide a stable and fast-paced development experience.
We use a 8 levels boot system.
All modules are scanned and put in a sequence with same-level modules
We cycle from level 0 to 7, initializing all the same-level modules in a concurrent way
If the module is reloadable, it will unwind and rewind in the same sequence on reloads
See the Contributing section for more information
| 行业焦点 NeuExcell Therapeutics完成上千万美元Pre-A轮融资。此轮融资由凯风创投领投,其他机构投资者包括元生创投、Oriza Seed、清源创投(Tsingyuan)和InnoAngel。NeuExcell是一家私营初创基因技术公司,在美国宾夕法尼亚州和中国上海分别设有总部。其使命是改善神经退行性疾病和CNS损伤患者的生活。公司以陈功教授的科学研究工作为基础, 通过基于
udi书的引论用了亨利 庞加莱的一段话,好奇,原来是大牛。。。 ai 快要受不了sb javaeye的图片上传了,不能插入本地图片,无语 links: http://zh.wikipedia.org/zh-hk/%E5%84%92%E5%8B%92%C2%B7%E6%98%82%E5%88%A9%C2%B7%E5%BA%9E%E5%8A%A0%E8%8E%B1 http://bai
这几天学习 JSON - SuperObject, 非常幸运地得到了其作者 Henri Gourvest 大师的同步指点! (Henri 大师也是 DSPack 和 GDI+ 头文件的作者; 大师是法国人, 竟能用中文给我回复, 没想到!). 学习中发现 SuperObject 有些地方对中文(或者说 Unicode)支持不是所期望的, 现在专贴提出来供大师鉴别. 以下例子都会出现乱码, 虽然都可
庞加莱,J. H.(Poincaré, Jules Henri)1854年4月29日生于法国南锡;1912年 7月17日卒于巴黎.数学、物理学、天体力学、科学哲学. 庞加莱的父亲莱昂(Léon,Poincaré)是一位第一流的生理学家兼医生、南锡医科 大学教授,母亲是一位善良、聪明的女性.庞加莱的叔父安托万(Antoine,Poincaré) 曾任国家道路桥梁部的检查官.庞加莱的堂弟雷蒙(Ra