宇天 2020-09-15 18:35:00 +08:00
commit ecce207db2
16 changed files with 1048 additions and 0 deletions

16
.gitignore vendored Normal file
View File

@ -0,0 +1,16 @@
.DS_Store
.AppleDouble
.LSOverride
.vscode
.idea
node_modules/
# Thumbnails
._*
# Files that might appear on external disk
.Spotlight-V100
.Trashes

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

130
Readme.md Normal file
View File

@ -0,0 +1,130 @@
# Five.js(node-five)
![give me five](http://attach.cdn.doui.cc/apps/five.jpg)
一个轻量级的,易学的,拓展性灵活的 nodejs MVC 框架, 5 分钟即可上手。取自"Give me five"之意, 一切就是这么简单
该分支要求 nodejs 版本在 7.0 或以上,默认使用 mongoDB/MySQL其他的数据库可以自行拓展
## 启用方法(步骤)
**注**
`本框架和用法 都是在 Linux 或者 Mac 下面测试通过。至于使用 Windows 并坚持玩新技术的同学,我坚信他们一定有着过人的、`
`甚至是不可告人的兼容性 bug 处理能力,所以这部分同学麻烦在安装过程无法继续时,自行兼容一下`
1. 下载安装 Five.js 框架。
* 为了方便下载安装及管理, 推荐使用 five-cli(这是一款专门为框架开发的脚本工具) 进行操作。
```bash
# 全局安装 five-cli
sudo npm i five-cli -g
# 进入项目目录
cd /project/demo
# 初始化一个项目,初始化完成会自动安装所需要的依赖
five-cli init
# 初始化完成之后, 执行以下命令即可启动了,如果需要修改配置,可以先修改好再启动
five-cli start
```
* 也可以自行通过 npm 安装, 自己构建启动配置
```bash
# 进入项目目录
cd /project/demo
npm i node-five --save
mkdir apps public data views
touch app.js
# 自行编辑app.js, 然后通过node, pm2启动项目即可
```
2. 配置框架
建立启动文件, 如 app.js
```javascript
'use strict'
const Five = require('five')
const app = new Five()
app.set({ website: 'www.your_domain.com' })
app.set({ domain: 'your_domain.com' }) // 设置域cookie用到不设置则同步website
app.set({ VIEWS: './views/' }) // [可选], 但是要用到模板渲染页面时, 必须指定
app.preload('./apps/') // [必须], 预加载应用目录
app.listen(3001) // 默认是3000
```
其他的配置, 请参考 `文档(全局配置)` 一节
3. 启动应用。在项目根目录打开终端, 输入以下命令 `five-cli start`, 然后根据提示操作, 即可
```bash
# 初始化完成之后, 执行以下命令即可启动了,如果需要修改配置,可以先修改好再启动
five-cli start
不是使用five-cli创建的项目, 可使用node/pm2等启动项目
node app.js
# or
pm2 start app.js
```
4. 添加 nginx 配置(使用其他 web 服务,如 apache 的童鞋,请自行根据所使用的 web 服务器语法改写**强烈推荐 nginx**), 路径啥的自行根据自己的机器修改
```nginx
upstream five_upstream {
server 127.0.0.1:3000;
#server 127.0.0.1:3005;
keepalive 64;
}
server {
listen 80;
server_name doui.cc;
index index.html index.htm;
root /www/doui.cc/public;
location ~ ^/(images/|js/|css/|cache/|favicon.ico|robots.txt) {
expires 1d;
access_log off;
}
location / {
try_files $uri
@proxy;
}
location @proxy {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
proxy_max_temp_file_size 0;
proxy_pass http://five_upstream;
proxy_redirect off;
proxy_read_timeout 240s;
}
}
```
5. Enjoy you web
## 版权说明
> 本框架使用 MIT 开源协议, 一切的使用,请遵循 MIT 协议。

202
index.js Normal file
View File

@ -0,0 +1,202 @@
/**
* 框架核心
* @authors yutent (yutent@doui.cc)
* @date 2015-11-25 18:06:14
*
*/
'use strict'
require('es.shim') // 加载拓展方法
var init = require('./lib/reg-init')
var log = console.log
var http = require('http')
var path = require('path')
var Request = require('http.request')
var Response = require('http.response')
var routerWare = require('./lib/middleware/router')
var cookieWare = require('./lib/middleware/cookie')
var sessionWare = require('./lib/middleware/session')
var credentialsWare = require('./lib/middleware/credentials')
function hideProperty(host, name, value) {
Object.defineProperty(host, name, {
value: value,
writable: true,
enumerable: false,
configurable: true
})
}
class Five {
constructor() {
hideProperty(this, '__FIVE__', Object.assign({}, init))
hideProperty(this, '__MODULES__', { __error__: null })
hideProperty(this, '__MIDDLEWARE__', [])
hideProperty(this, '__INSTANCE__', {})
global.libs = {
Smarty: require('smartyx'), //模板引擎
Log: require('./lib/module/log'), //基础日志记录工具
Email: require('./lib/module/sendmail'), //加载email发送类
Mysql: require('mysqli'), //加载mysql操作类
Ioredis: require('ioredis')
}
global.Util = {
sec: require('crypto.js'),
path: require('path'),
url: require('url'),
fs: require('iofs'),
child: require('child_process')
}
global.Controller = require('./lib/controller')
}
__init__() {
var { domain, website, session } = this.__FIVE__
domain = domain || website
session.domain = session.domain || domain
this.set({ domain, session })
// 这里只创建session的存储器, 而初始化操作在中间件中进行
if (session.type === 'redis') {
hideProperty(
this,
'__SESSION_STORE__',
new libs.Ioredis({
host: session.db.host || '127.0.0.1',
port: session.db.port || 6379,
db: session.db.db || 0
})
)
} else {
hideProperty(this, '__SESSION_STORE__', {})
}
// 将session和cookie的中间件提到最前
// 以便用户自定义的中间件可以直接操作session和cookie
this.__MIDDLEWARE__.unshift(sessionWare)
this.__MIDDLEWARE__.unshift(cookieWare)
this.__MIDDLEWARE__.unshift(credentialsWare)
this.use(routerWare)
}
/*------------------------------------------------------------------------*/
// 注册属性到全局Five对象
set(obj) {
for (let i in obj) {
if (typeof obj[i] === 'object' && !Array.isArray(obj[i])) {
if (!this.__FIVE__[i]) {
this.__FIVE__[i] = obj[i]
} else {
try {
Object.assign(this.__FIVE__[i], obj[i])
} catch (err) {
log(err)
}
}
} else {
this.__FIVE__[i] = obj[i]
}
}
return this
}
// 获取全局配置
get(key) {
try {
return new Function('o', `return o.${key}`)(this.__FIVE__)
} catch (err) {
return
}
}
// 加载中间件/缓存模块
// 与别的中间件用法有些不一样, 回调的传入参数中的req和res,
// 并非原生的request对象和response对象,
// 而是框架内部封装过的,可通过origin属性访问原生的对象
use(key, fn) {
if (arguments.length === 1) {
if (typeof key !== 'function') {
throw TypeError('argument 1 must be a callback')
}
this.__MIDDLEWARE__.push(key)
} else {
if (typeof key !== 'string') {
return
}
libs[key] = fn
}
}
// 预加载应用
preload(dir) {
var list = Util.fs.ls(dir)
if (list) {
list.forEach(file => {
var { name } = path.parse(file)
if (name.startsWith('.')) {
return
}
try {
this.__MODULES__[name] = require(file)
} catch (err) {
this.__MODULES__.__error__ = err
}
})
}
return this
}
// 注册实例化对象到实例池中
// 与use方法不同的是, 这个会在server创建之前就已经执行
ins(name, fn) {
var _this = this
if (arguments.length === 1) {
return this.__INSTANCE__[name]
}
if (typeof fn === 'function') {
fn.call(this, this.__FIVE__, function next(instance) {
if (instance) {
_this.__INSTANCE__[name] = instance
}
})
}
}
// 启动http服务
listen(port) {
var _this = this
this.__init__()
var server = http.createServer(function(req, res) {
var response = new Response(req, res)
var request = new Request(req, res)
response.set('X-Powered-By', 'Five.js')
var middleware = _this.__MIDDLEWARE__.concat()
var fn = middleware.shift()
if (fn) {
;(async function next() {
await fn.call(_this, request, response, function() {
fn = middleware.shift()
if (fn) {
next()
}
})
})()
}
})
server.listen(port || this.get('port'))
return server
}
}
module.exports = Five

79
lib/controller.js Normal file
View File

@ -0,0 +1,79 @@
/**
* 控制器基类
* @authors yutent (yutent@doui.cc)
* @date 2016-01-02 23:19:16
*
*/
'use strict'
const smarty = new libs.Smarty()
const jwt = require('./module/jwt')
class Controller {
constructor({ ctx, req, res }) {
this.ctx = ctx
this.name = req.app
this.request = req
this.response = res
this.jwt = {
sign: jwt.sign.bind(this),
result: jwt.verify.call(this)
}
smarty.config('path', this.ctx.get('VIEWS'))
smarty.config('ext', this.ctx.get('temp_ext'))
smarty.config('cache', !!this.ctx.get('temp_cache'))
this.cookie = this.ctx.ins('cookie')
this.session = this.ctx.ins('session')
}
//定义一个变量类似于smarty把该
assign(key, val) {
key += ''
if (!key) {
return
}
if (val === undefined || val === null) {
val = ''
}
smarty.assign(key, val)
}
//模板渲染, 参数是模板名, 可不带后缀, 默认是 .tpl
render(file, noParse = false) {
smarty
.render(file, noParse)
.then(html => {
this.response.render(html)
})
.catch(err => {
this.response.error(err)
})
}
// RESFULL-API规范的纯API返回
send(status = 200, msg = 'success', data = {}) {
if (typeof msg === 'object') {
data = msg
msg = 'success'
}
this.response.send(status, msg, data)
}
//针对框架定制的debug信息输出
xdebug(err) {
let msg = err
if (this.ctx.get('debug')) {
msg = err.stack || err
}
this.response.append('X-debug', msg + '')
}
}
module.exports = Controller

38
lib/middleware/cookie.js Normal file
View File

@ -0,0 +1,38 @@
/**
*
* @authors yutent (yutent@doui.cc)
* @date 2018-05-26 00:01:00
* @version $Id$
*/
const Cookie = require('http.cookie')
module.exports = function(req, res, next) {
var cookie = new Cookie(req.origin.req, req.origin.res)
var domain = this.get('domain')
this.__INSTANCE__.cookie = function(key, val, opt) {
if (typeof key !== 'string') {
throw new Error(
`argument key must be a string in cookie() ${typeof key} given`
)
}
if (arguments.length === 1) {
return cookie.get(key)
}
if (!opt) {
opt = {}
}
opt.domain = opt.domain || domain
val += ''
if (!val) {
opt.expires = opt.maxAge = -1
}
cookie.set(key, val, opt)
}
next()
}

View File

@ -0,0 +1,37 @@
/**
*
* @authors yutent (yutent@doui.cc)
* @date 2018-09-03 22:26:51
*/
'use strict'
module.exports = function(req, res, next) {
var supportCredentials = this.get('supportCredentials')
var credentialsRule = this.get('credentialsRule')
var credentialsMaxAge = this.get('credentialsMaxAge')
if (supportCredentials) {
var origin = req.header('origin') || req.header('referer') || ''
var headers = req.header('access-control-request-headers')
origin = Util.url.parse(origin)
if (credentialsRule && origin.hostname) {
if (!credentialsRule.test(origin.hostname)) {
return res.end('')
}
}
res.set('Access-Control-Allow-Credentials', 'true')
res.set('Access-Control-Allow-Origin', `${origin.protocol}//${origin.host}`)
if (headers) {
res.set('Access-Control-Allow-Headers', headers)
}
if (credentialsMaxAge) {
res.set('Access-Control-Max-Age', credentialsMaxAge)
}
if (req.method === 'OPTIONS') {
return res.end('')
}
}
next()
}

53
lib/middleware/router.js Normal file
View File

@ -0,0 +1,53 @@
/**
* 路由
* @authors yutent (yutent@doui.cc)
* @date 2015-10-01 19:11:19
*
*/
'use strict'
module.exports = function(req, res, next) {
if (!this.__MODULES__[req.app]) {
if (!this.__MODULES__.__error__) {
res.error(`The app [${req.app}] not found`, 404)
} else {
res.error(
this.get('debug')
? this.__MODULES__.__error__.stack
: this.__MODULES__.__error__,
500
)
}
return
}
try {
if (req.path.length < 1) {
req.path.push('index')
}
var app = new this.__MODULES__[req.app]({ ctx: this, req, res })
if (this.get('routeMode') === 1) {
var act = req.path.shift()
if (app[act + 'Action']) {
app[act + 'Action'].apply(app, req.path).catch(err => {
res.error(this.get('debug') ? err.stack || err : err, 500)
})
} else {
res.error(`Action[${act}] not found`, 404)
}
} else {
if (app.indexAction) {
app.indexAction.apply(app, req.path).catch(err => {
res.error(this.get('debug') ? err.stack || err : err, 500)
})
} else {
res.error(`Default Action not found`, 404)
}
}
} catch (err) {
res.error(this.get('debug') ? err.stack || err : err, 500)
}
}

65
lib/middleware/session.js Normal file
View File

@ -0,0 +1,65 @@
/**
*
* @authors yutent (yutent@doui.cc)
* @date 2018-07-26 15:50:25
* @version $Id$
*/
const redisStore = require('../module/redis-store')
const nativeStore = require('../module/native-store')
module.exports = function(req, res, next) {
var opt = this.get('session')
var jwt = this.get('jwt')
var cookie = this.ins('cookie')
var session = null
var uuid = Util.sec.uuid()
var ssid = ''
opt.jwt = jwt
if (req.method === 'OPTIONS') {
return next()
}
if (jwt) {
var auth = req.header('authorization')
if (auth) {
ssid = auth.split('.').pop()
uuid = auth
}
} else {
ssid = cookie('NODESSID')
// 校验级别为1, 则混入ua
if (opt.level > 0) {
uuid += req.header('user-agent')
}
// 校验级别为2, 则混入ip
if (opt.level > 1) {
uuid += req.ip()
}
}
uuid = Util.sec.sha1(uuid)
if (opt.type === 'redis') {
session = new redisStore(this.__SESSION_STORE__, opt, uuid)
} else {
session = new nativeStore(this.__SESSION_STORE__, opt, uuid)
}
// 启用SESSION
// ssid非法或过期时需要重写
if (!ssid || ssid !== session.start(ssid)) {
ssid = session.start(ssid)
if (!jwt) {
cookie('NODESSID', ssid, {
httpOnly: true,
expires: opt.ttl,
domain: opt.domain
})
}
}
this.__INSTANCE__.session = session
next()
}

63
lib/module/jwt.js Normal file
View File

@ -0,0 +1,63 @@
/**
*
* @authors yutent (yutent@doui.cc)
* @date 2018-08-24 15:24:56
*/
'use strict'
const sha256 = (str, secret) => {
return Util.sec
.hmac('sha256', str, secret, 'base64')
.replace(/[+\/]/g, m => {
return m === '+' ? '-' : '_'
})
.replace(/=/g, '')
}
const sign = function(token) {
// "{"typ":"JWT","alg":"HS256"}"
// 这里固定使用sha256
var header = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'
var opt = this.ctx.get('session')
var secret = this.ctx.get('jwt')
// 加入过期时间, 同session.ttl
var payload = { token, expires: Date.now() + opt.ttl * 1000 }
payload = JSON.stringify(payload)
payload = Util.sec.base64encode(payload, true)
var auth = sha256(`${header}.${payload}`, secret)
this.ctx.ins('session').start(auth)
return `${header}.${payload}.${auth}`
}
const verify = function() {
var jwt = this.request.header('authorization') || ''
var secret = this.ctx.get('jwt')
var auth, token, payload
jwt = jwt.split('.')
if (!secret || jwt.length !== 3) {
return false
}
auth = jwt.pop()
token = JSON.parse(Util.sec.base64decode(jwt[1], true))
// 如果已经过期, 则不再校验hash
if (Date.now() > token.expires) {
return 'expired'
}
payload = jwt.join('.')
if (sha256(payload, secret) === auth) {
return token.token
}
return false
}
module.exports = {
verify,
sign
}

48
lib/module/log.js Normal file
View File

@ -0,0 +1,48 @@
/**
*
* @authors yutent (yutent@doui.cc)
* @date 2015-11-25 17:48:17
*
*/
'use strict'
class Log {
constructor(file = 'run_time.log', dir) {
if (!dir) {
throw new Error(`agument dir must be a string, but ${typeof dir} given.`)
}
if (!Util.fs.exists(dir)) {
Util.fs.mkdir(dir)
}
this.file = Util.path.resolve(dir, file)
}
error(str) {
this.save(str, 'error')
}
info(str) {
this.save(str, 'info')
}
warn(str) {
this.save(str, 'warning')
}
debug(str) {
this.save(str, 'debug')
}
//写入日志文件
save(str, type) {
type = type || 'debug'
Util.fs.origin.appendFile(
`[${type}] ${new Date().format('Y-m-d_H:i:s')} ${str} \n`,
this.file
)
}
}
module.exports = Log

View File

@ -0,0 +1,83 @@
/**
*
* @authors yutent (yutent@doui.cc)
* @date 2016-03-15 16:01:38
*
*/
'use strict'
function hideProperty(host, name, value) {
Object.defineProperty(host, name, {
value: value,
writable: true,
enumerable: false,
configurable: true
})
}
class Session {
constructor(store, opt, uuid) {
this.opt = opt
this.uuid = uuid
this.store = store
}
createSsid(ssid) {
if (ssid) {
if (!this.opt.jwt && this.opt.level > 0 && ssid !== this.uuid) {
ssid = this.uuid
}
} else {
ssid = this.uuid
}
this.ssid = ssid
if (
!this.store.hasOwnProperty(ssid) ||
this.store[ssid].__EXPIRES__ < Date.now()
) {
this.store[ssid] = {}
}
//设置session有效期
hideProperty(
this.store[ssid],
'__EXPIRES__',
Date.now() + this.opt.ttl * 1000
)
}
start(ssid) {
this.createSsid(ssid)
return this.ssid
}
// 获取session字段值
get(key) {
return key ? this.store[this.ssid][key] || null : this.store[this.ssid]
}
// 设置session字段值
set(key, val) {
if (typeof key === 'object') {
for (let i in key) {
this.store[this.ssid][i] = key[i]
}
} else {
this.store[this.ssid][key] = val
}
}
// 删除单个字段
unset(key) {
if (this.store[this.ssid].hasOwnProperty(key)) {
delete this.store[this.ssid][key]
}
}
// 清除个人session
clear() {
this.store[this.ssid] = {}
}
}
module.exports = Session

83
lib/module/redis-store.js Normal file
View File

@ -0,0 +1,83 @@
/**
* Session类, 存入store
* @authors yutent (yutent@doui.cc)
* @date 2016-03-14 16:08:57
*
*/
'use strict'
class Session {
constructor(store, opt, uuid) {
this.store = store
this.opt = opt
this.uuid = uuid
}
createSsid(ssid) {
if (ssid) {
if (!this.opt.jwt && this.opt.level > 0 && ssid !== this.uuid) {
ssid = this.uuid
}
} else {
ssid = this.uuid
}
this.ssid = ssid
// 设置session有效期
this.store.expire(ssid, this.opt.ttl)
}
start(ssid) {
this.createSsid(ssid)
return this.ssid
}
// 获取session字段值, 需要await指令
get(key) {
var defer = Promise.defer()
this.store.hgetall(this.ssid, (err, obj) => {
if (err) {
return defer.reject(err)
}
for (let i in obj) {
if (!obj[i]) {
continue
}
obj[i] = Number.parse(obj[i])
}
//不传key时,直接返回全部字段
if (!key) {
defer.resolve(obj)
} else {
defer.resolve(obj.hasOwnProperty(key) ? obj[key] : null)
}
})
return defer.promise
}
//设置session字段值
set(key, val) {
if (typeof key === 'object') {
for (let i in key) {
this.store.hset(this.ssid, i, key[i])
}
} else {
this.store.hset(this.ssid, key, val)
}
}
//删除单个字段
unset(key) {
this.store.hdel(this.ssid, key)
}
//清除个人session
clear() {
this.store.del(this.ssid)
}
}
module.exports = Session

48
lib/module/sendmail.js Normal file
View File

@ -0,0 +1,48 @@
/**
*
* @authors yutent (yutent@doui.cc)
* @date 2015-11-27 10:50:16
*
*/
'use strict'
const mailx = require('mailx')
class Sendmail {
constructor({ host, port, mail, passwd } = {}) {
if (!host || !port || !mail || !passwd) {
throw new Error('smtp options [host, port, mail, passwd] is required.')
}
this.smtp = mailx.transport(host, port, mail, passwd)
this.mail = mailx.message()
}
// 发件人
from(info) {
this.mail.setFrom(info.name, info.mail)
return this
}
// 收件人
to(info) {
this.mail.addTo(info.name, info.mail)
return this
}
// 发送正文
send(mail) {
this.mail.setSubject(mail.subject)
this.mail.setHtml(mail.content)
var defer = Promise.defer()
this.smtp.send(this.mail, function(err, res) {
if (err) {
defer.reject(err)
} else {
defer.resolve(res)
}
})
return defer.promise
}
}
module.exports = Sendmail

52
lib/reg-init.js Normal file
View File

@ -0,0 +1,52 @@
/**
*
* @authors yutent (yutent@doui.cc)
* @date 2016-05-15 14:37:47
*
*/
'use strict'
const init = {
db: {},
session: {
type: 'native', // native 或 redis
ttl: 3600 * 24 * 7,
domain: '', // NODESSID域, 默认等于domain
level: 0, // 校验级别, 0: 不校验客户端, 1: 校验UA, 2: 校验UA+IP
db: {
host: '127.0.0.1',
port: 6379,
db: 0
}
},
website: 'localhost',
domain: '', // cookie域, 默认等于website
port: 3000,
routeMode: 1,
env: process.env.NODE_ENV === 'production' ? 'production' : 'development',
debug: process.env.NODE_ENV !== 'production', // debug模式
smtp: {
host: 'smtp.example.com',
port: 25,
mail: 'service@example.com',
name: '系统邮件',
passwd: ''
},
supportCredentials: false,
credentialsRule: null,
credentialsMaxAge: 0,
jwt: null, // jwt secret
regexp: {
// 常用正则
email: /^[\w\.\-]+@\w+([\.\-]\w+)*\.\w+$/,
uname: /^[A-Za-z\d_]{4,16}$/,
passwd: /^[\S]{6,20}$/,
phone: /^1[34578]\d{9}$/,
idCard: /^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}([0-9]|X|x)$/,
CN: /^[\u4e00-\u9fa5]+$/,
qq: /^\d{5,12}$/
}
}
module.exports = init

30
package.json Normal file
View File

@ -0,0 +1,30 @@
{
"name": "node-five",
"version": "3.2.6",
"type": "module",
"description": "Five.js, 一个轻量级的nodejs mvc框架 旨在简单易用, 5分钟即可上手",
"author": "宇天 <yutent@doui.cc>",
"main": "index.js",
"dependencies": {
"crypto.js": "^1.3.1",
"es.shim": "^1.1.2",
"iofs": "^1.3.2",
"mysqli": "^3.0.11",
"http.request": "^1.1.0",
"http.response": "^1.0.2",
"http.cookie": "^1.0.2",
"smartyx": "^1.3.1",
"ioredis": "^3.2.2",
"mailx": "^0.0.11"
},
"devDependencies": {},
"repository": {
"type": "git",
"url": "https://github.com/yutent/five.git"
},
"keywords": ["five, fivejs, node-five, five.js, nodejs, mvc, koa, express"],
"engines": {
"node": ">=8.0.0"
},
"license": "MIT"
}