jj.js 0.8.3 → 0.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,7 +30,7 @@ A simple and lightweight MVC framework built on nodejs+koa2(一个基于nodejs
30
30
  npm i jj.js
31
31
  ```
32
32
 
33
- > 运行环境要求:node.js >= v12
33
+ > 运行环境要求:node.js >= v12 mysql >= 5.3
34
34
 
35
35
  ## Hello world !
36
36
 
@@ -105,7 +105,6 @@ node server.js
105
105
  - **public**:静态资源目录,主要存放css文件、js文件、图片等静态资源。在`./config/app.js`中通过`static_dir`参数可以设置或更改静态目录名字,为空时,则关闭静态访问(系统不会加载静态资源访问的逻辑,最大节省内容,也即所谓的轻量)
106
106
  - **server.js**:应用入口文件,名字可以任意改。
107
107
 
108
- >
109
108
  ### 系统类库
110
109
 
111
110
  ```javascript
@@ -276,6 +275,16 @@ module.exports = Index;
276
275
 
277
276
  > 注意:所有的输出和跳转并不会阻止后续代码执行,所以要终止代码执行,需在前面加上`return`。
278
277
 
278
+ > 方法:**async _init()** 控制器初始化
279
+
280
+ 异步方法,除了使用控制器中间件执行一些初始化或验证外,控制器提供了这个更简单的方法。一旦控制器定义了这个方法,在通过url访问控制器其他公开方法时,在执行完控制器中间件后,都会先执行这个函数,如果想终止后续代码执行,可以在方法里返回`false`。
281
+
282
+ > 方法:**async _end()** 控制器结束
283
+
284
+ 如果定义了这个函数,在通过url访问控制器其他公开方法后,会调用这个函数。
285
+
286
+ > 注意:因为系统默认注册了 `应用/控制器/方法` 的全局路由,所以控制器里的方法都能被直接访问,如果想作为私有方法不被访问,可以在方法前加下划线`_`前缀。
287
+
279
288
  ### Middleware中间件类
280
289
 
281
290
  > 方法:**$show(content)** 参照控制器类介绍
@@ -290,9 +299,9 @@ module.exports = Index;
290
299
 
291
300
  ### Db数据库类
292
301
 
293
- > 方法:**async startTrans(async fun)** 开启事务
302
+ > 方法:**async startTrans(fun)** 开启事务
294
303
 
295
- 异步方法,如果fun不为空,并且是函数,则开启事务后,会自动执行此函数,并提交事务,不用再手工提交或回滚事务。
304
+ 异步方法,如果`fun`不为空,并且是函数,则开启事务后,会自动执行此函数,并提交事务,不用再手工提交或回滚事务。`fun`为异步函数。
296
305
 
297
306
  > 方法:**async commit()** 提交事务
298
307
 
@@ -727,9 +736,9 @@ module.exports = route;
727
736
 
728
737
  应用配置不用每项都设置,只设置自己需要改的,默认会继承框架的默认配置,在控制器、中间件、模板类、模型类里都可以通过`this.$config.xxx`使用。
729
738
 
730
- app配置`.config/app.js`:
739
+ - app配置`.config/app.js`:
731
740
  ```javascript
732
- {
741
+ const app = {
733
742
  app_debug: true, // 调试模式
734
743
  app_multi: false, // 是否开启多应用
735
744
 
@@ -744,20 +753,22 @@ app配置`.config/app.js`:
744
753
 
745
754
  koa_body: null // koa-body配置参数,为''、null、false时,关闭koa-body,此时upload类将不可用,并且系统接收不到post参数。启用的话,建议配置:{multipart: true, formidable: {keepExtensions: true, maxFieldsSize: 10 * 1024 * 1024}}
746
755
  }
756
+ module.exports = app;
747
757
  ```
748
- 模板配置`.config/view.js`:
758
+ - 模板配置`.config/view.js`:
749
759
  ```javascript
750
- {
760
+ const view = {
751
761
  view_folder: 'view', // 模板目录名
752
762
  view_depr: '/', // 模版文件名分割符,'/'代表二级目录
753
763
  view_ext: '.htm', // 模版文件后缀
754
764
  view_engine: 'art-template', // 默认模版引擎,字符串或引擎类,咱不支持更换
755
765
  view_filter: {}, // 模版函数,配置注入模板的函数,默认会自动注入url函数
756
766
  }
767
+ module.exports = view;
757
768
  ```
758
- 数据库配置`.config/db.js`:可以配置多个,方便程序里切换使用。
769
+ - 数据库配置`.config/db.js`:可以配置多个,方便程序里切换使用。
759
770
  ```javascript
760
- {
771
+ const db = {
761
772
  default: {
762
773
  type : 'mysql', // 数据库类型
763
774
  host : '127.0.0.1', // 服务器地址
@@ -769,24 +780,27 @@ app配置`.config/app.js`:
769
780
  prefix : 'jj_' // 数据库表前缀
770
781
  }
771
782
  }
783
+ module.exports = db;
772
784
  ```
773
- 日志配置`.config/log.js`:log_handle可以自定义日志handle
785
+ - 日志配置`.config/log.js`:log_handle可以自定义日志handle
774
786
  ```javascript
775
- {
787
+ const log = {
776
788
  log_level: [], // [error, warning, info, debug, http, sql]
777
789
  log_handle: function(msg, level) {console.log(`[${format('YY-mm-dd HH:ii:ss')}] [${level}] ${typeof msg == 'String' ? msg : JSON.stringify(msg)}`);} //function(msg, level) {}
778
790
  }
791
+ module.exports = log;
779
792
  ```
780
- 缓存配置`.config/cache.js`:
793
+ - 缓存配置`.config/cache.js`:
781
794
  ```javascript
782
- {
795
+ const cache = {
783
796
  cache_time: 60 * 60 * 24, // 默认缓存时间(1天),为空或false则为10年
784
797
  clear_time: undefined // (undefined: 一天清理一次, 0: 关闭自动清理, >0: 为自动清理周期)
785
798
  }
799
+ module.exports = cache;
786
800
  ```
787
- 分页配置`.config/page.js`:
801
+ - 分页配置`.config/page.js`:
788
802
  ```javascript
789
- {
803
+ const page = {
790
804
  page_key : 'page', // 默认分页标识
791
805
  key_origin : 'query', // query 或 params
792
806
  page_size : 10, // 默认分页大小
@@ -810,22 +824,65 @@ app配置`.config/app.js`:
810
824
  //渲染模版
811
825
  template : '<div class="pagination"><ul class="page">${index}${prev}${list}${next}${end}</ul>${info}</div>'
812
826
  }
827
+ module.exports = page;
813
828
  ```
814
- 跳转模板配置`.config/tpl.js`:模板可以配置为自定义的
829
+ - 跳转模板配置`.config/tpl.js`:模板可以配置为自定义的
815
830
  ```javascript
816
- {
831
+ const tpl = {
817
832
  jump: require('./tpl/jump'), // 跳转模板
818
833
  exception: require('./tpl/exception') // 异常页面模板
819
834
  }
835
+ module.exports = tpl;
820
836
  ```
821
- 自定义配置`.config/self.js`:自定义配置同样可以直接通过`this.$config.self`使用。
837
+ - 自定义配置`.config/self.js`:自定义配置同样可以直接通过`this.$config.self`使用。
822
838
  ```javascript
823
- {
839
+ const self = {
824
840
  option1: ''
825
841
  option2: ''
826
842
  ...
827
843
  }
844
+ module.exports = self;
845
+ ```
846
+ - 路由配置`.config/routes.js`:
847
+ 路由功能基于`@koa/router`开发,关于url匹配规则可以参考官方文档:[文档地址](https://www.npmjs.com/package/@koa/router)
848
+
849
+ > 本框架默认内置 `应用/控制器/方法` 的全局路由,即如果不需要定制url,可以直接访问,无需配置路由。
850
+
851
+ > 路由配置为一个数组,每一项为一条规则,项属性包含url规则,path访问路径,name规则名字,type路由类型。url一旦匹配,即会停止,如果需要继续向下匹配,需在程序内调用 `await this.$next();`,路由配置参考示例:
852
+ ```javascript
853
+ routes = [
854
+ {url: '/', path: 'app/index/index2'}, // 访问'/',会定位到app应用的index控制的index2方法
855
+ {url: '/article/:id.html', path: 'app/article/article', name: 'article'}, // 访问'/article/123.html',会定位到app应用的article控制的article方法;通过this.ctx.params.id可以获取到参数id;通过article可以反编译网址,执行this.$url.build(':article', {id: 123}); 生成'/article/123.html'
856
+ {url: '/admin', path: 'app/admin/check', type: 'middleware'}, // 访问'/admin',会定位到app应用的admin中间件的check方法
857
+ {url: '/about', path: 'app/about/index', type: 'view'}, // 访问'/about',会定位到app应用的view模板目录about目录下的index.htm文件,并直接输出文件内容
858
+ {url: '/:cate/list_:page.html', path: 'cate/cate', name: 'cate_page'}, // 多参数分页示例
859
+ {url: '/hello', path: async (ctx, next) => {ctx.body = 'hello world, hello jj.js!';}} // 直接路由到函数
860
+ ];
861
+
862
+ module.exports = routes;
863
+ ```
864
+ > 注意:本路由配置示例前两条规则为常规用法,后面的非常规用法,不建议使用。如果是单应用模式,可以去掉path参数里的app。
865
+
866
+ ### 应用入口文件
867
+
868
+ 应用入口文件放在项目的根目录,名字可以任意改。
869
+
870
+ 示例文件 `./server.js`
871
+ ```javascript
872
+ const {app, Logger} = require('jj.js');
873
+
874
+ app.run(3000, '127.0.0.1', function(err){
875
+ !err && Logger.info('http server is ready on 3000');
876
+ });
828
877
  ```
878
+ 其中`app`是一个koa实例,可以使用`use`方法添加应用级中间件,使用方法参考koa文档。`app`的`run`方法经过了封装,仅允许调用一次。其中`Logger`因为是一个静态类,所以可以直接调用`info`方法输出日志。
879
+
880
+ ## 总结
881
+
882
+ 通过以上文档,可以看到:
883
+ 1. jj.js是一个轻量的mvc框架,几乎所有文件不使用就不会调用。
884
+ 2. 同时也是个功能强大的框架,支持应用级、路由级、控制器级三级中间件。
885
+ 3. 类库自动加载,只要是继承自`Context`的类库,都可以在方法内使用包含`$`前缀的属性自动加载其他资源。
829
886
 
830
887
  ## 应用案例
831
888
 
package/lib/db.js CHANGED
@@ -66,10 +66,10 @@ class Db extends Context
66
66
  if(pool.has(this._config)) {
67
67
  pool.get(this._config).end(err => {
68
68
  if(err) {
69
- const message = '连接池销毁失败:' + err['message'];
69
+ const message = '连接池销毁失败:' + err.message;
70
70
  this.$logger.sql(message);
71
71
  this.$logger.error(message);
72
- throw new Error(message);
72
+ throw new Error('DbError: ' + message);
73
73
  } else {
74
74
  pool.delete(this._config);
75
75
  this.$logger.sql(`连接池销毁成功:{poolTotal: ${pool.size}}`);
@@ -95,10 +95,10 @@ class Db extends Context
95
95
  return new Promise((resolve, reject) => {
96
96
  p.getConnection((err, connection) => {
97
97
  if(err) {
98
- const message = '获取数据库连接失败:' + err['message'];
98
+ const message = '获取数据库连接失败:' + err.message;
99
99
  this.$logger.sql(message);
100
100
  this.$logger.error(message);
101
- reject(new Error(message));
101
+ reject(new Error('DbError: ' + message));
102
102
  } else {
103
103
  this.$logger.sql(`获取数据库连接:{limit: ${p.config.connectionLimit}, all: ${p._allConnections.length}, acquiring: ${p._acquiringConnections.length}, free: ${p._freeConnections.length}, queue: ${p._connectionQueue.length}}`);
104
104
  resolve(connection);
@@ -134,10 +134,10 @@ class Db extends Context
134
134
  return new Promise((resolve, reject) => {
135
135
  conn.beginTransaction(async (err) => {
136
136
  if(err) {
137
- const message = '开启事务失败:' + err['message'];
137
+ const message = '开启事务失败:' + err.message;
138
138
  this.$logger.sql(message);
139
139
  this.$logger.error(message);
140
- reject(new Error(message));
140
+ reject(new Error('DbError: ' + message));
141
141
  } else {
142
142
  const message = '开启事务成功!';
143
143
  this.$logger.sql(message);
@@ -195,11 +195,11 @@ class Db extends Context
195
195
  return new Promise((resolve, reject) => {
196
196
  conn.commit((err) => {
197
197
  if(err) {
198
- const message = '事务提交失败:' + err['message'];
198
+ const message = '事务提交失败:' + err.message;
199
199
  this.$logger.sql(message);
200
200
  this.$logger.error(message);
201
201
  this.rollback();
202
- reject(new Error(message));
202
+ reject(new Error('DbError: ' + message));
203
203
  } else {
204
204
  trans.delete(this.ctx);
205
205
  const message = '事务提交成功!';
@@ -220,10 +220,10 @@ class Db extends Context
220
220
  return new Promise((resolve, reject) => {
221
221
  conn.query(sql, params, (err, data) => {
222
222
  if(err) {
223
- const message = '数据操作失败,SQL:' + this.format(sql, params) + '(' + err['message'] + ')';
223
+ const message = '数据操作失败,' + err.message + "\r\nSQL:" + this.format(sql, params);
224
224
  this.$logger.sql(message);
225
225
  this.$logger.error(message);
226
- reject(new Error(message));
226
+ reject(new Error('DbError: ' + message));
227
227
  } else {
228
228
  this.$logger.sql('数据操作成功,SQL:' + this.format(sql, params));
229
229
  resolve(data);
@@ -506,7 +506,7 @@ class Db extends Context
506
506
  if(!where[0]) {
507
507
  const message = 'update方法必须传入where参数';
508
508
  this.$logger.sql(message);
509
- throw new Error(message);
509
+ throw new Error('DbError: ' + message);
510
510
  }
511
511
  params = params.concat(where[1]);
512
512
 
@@ -539,7 +539,7 @@ class Db extends Context
539
539
  if(!where[0]) {
540
540
  const message = 'delete方法必须传入where参数';
541
541
  this.$logger.sql(message);
542
- throw new Error(message);
542
+ throw new Error('DbError: ' + message);
543
543
  }
544
544
  params = params.concat(where[1]);
545
545
 
package/lib/router.js CHANGED
@@ -9,7 +9,7 @@ if (cfg_routes) {
9
9
  for(let item of cfg_routes){
10
10
  item.method || (item.method = 'all');
11
11
  if(!~methods.indexOf(item.method)) {
12
- throw new Error(`未知路由方法:${item.method}`);
12
+ throw new Error(`RouteError: 未知路由方法:${item.method}`);
13
13
  }
14
14
  if(typeof item.path === 'function'){
15
15
  router[item.method](item.url, item.path);
package/lib/run.js CHANGED
@@ -7,9 +7,13 @@ const compose = require('koa-compose');
7
7
  const Logger = require('./logger');
8
8
 
9
9
  async function run(ctx, next, control_type=cfg_app.controller_folder) {
10
- Logger.debug(ctx.request);
11
- Logger.debug(`Router: {APP: '${ctx.APP}', CONTROLLER: '${ctx.CONTROLLER}', ACTION: '${ctx.ACTION}'}`);
12
- Logger.debug('Params/Get/Post:', ctx.params, ctx.query, ctx.request.body);
10
+ if(cfg_app.app_debug) {
11
+ Logger.debug(ctx.request);
12
+ Logger.debug(`Router(${control_type}): {APP: '${ctx.APP}', CONTROLLER: '${ctx.CONTROLLER}', ACTION: '${ctx.ACTION}'}`);
13
+ Object.keys(ctx.params).length && Logger.debug({Params: ctx.params});
14
+ Object.keys(ctx.query).length && Logger.debug({Get: ctx.query});
15
+ Object.keys(ctx.request.body || {}).length && Logger.debug({Post: ctx.request.body});
16
+ }
13
17
 
14
18
  // 设置根加载器
15
19
  ctx.$ = loader('./', ctx);
@@ -26,12 +30,12 @@ async function run(ctx, next, control_type=cfg_app.controller_folder) {
26
30
 
27
31
  // 应用、控制器、方法名字验证
28
32
  if (~ctx.APP.indexOf('.') || ~ctx.CONTROLLER.indexOf('.') || ~ctx.ACTION.indexOf('.')) {
29
- throw new Error(`应用、控制器或方法名字不合法!`);
33
+ throw new Error(`RunError: 应用、控制器或方法名字不合法!`);
30
34
  }
31
35
 
32
36
  if (ctx.APP[0] == '_' || ctx.APP[0] == '$' || !ctx._[ctx.APP]) {
33
37
  if(cfg_app.app_debug) {
34
- throw new Error(`应用:${ctx.APP}不存在!`);
38
+ throw new Error(`RunError: 应用:${ctx.APP}不存在!`);
35
39
  } else {
36
40
  return false;
37
41
  }
@@ -39,7 +43,7 @@ async function run(ctx, next, control_type=cfg_app.controller_folder) {
39
43
 
40
44
  if (!ctx._[ctx.APP][control_type]) {
41
45
  if(cfg_app.app_debug) {
42
- throw new Error(`目录:${ctx.APP}/${control_type}不存在!`);
46
+ throw new Error(`RunError: 目录:${ctx.APP}/${control_type}不存在!`);
43
47
  } else {
44
48
  return false;
45
49
  }
@@ -49,7 +53,7 @@ async function run(ctx, next, control_type=cfg_app.controller_folder) {
49
53
  if(control_type == cfg_view.view_folder) {
50
54
  const content = await readFile(path.join(ctx._[ctx.APP][control_type].__node.path, ctx.CONTROLLER + cfg_view.view_depr + ctx.ACTION + cfg_view.view_ext));
51
55
  if(!content) {
52
- throw new Error(`模板文件:${ctx.APP}/${control_type}/${ctx.CONTROLLER + cfg_view.view_depr + ctx.ACTION + cfg_view.view_ext}不存在!`);
56
+ throw new Error(`RunError: 模板文件:${ctx.APP}/${control_type}/${ctx.CONTROLLER + cfg_view.view_depr + ctx.ACTION + cfg_view.view_ext}不存在!`);
53
57
  }
54
58
  ctx.body = content;
55
59
  return;
@@ -60,7 +64,7 @@ async function run(ctx, next, control_type=cfg_app.controller_folder) {
60
64
 
61
65
  if (ctx.CONTROLLER[0] == '_' || ctx.CONTROLLER[0] == '$' || typeof Controller != 'function') {
62
66
  if(cfg_app.app_debug) {
63
- throw new Error(`class文件:${ctx.APP}/${control_type}/${ctx.CONTROLLER}不存在!`);
67
+ throw new Error(`RunError: class文件:${ctx.APP}/${control_type}/${ctx.CONTROLLER}不存在!`);
64
68
  } else {
65
69
  return false;
66
70
  }
@@ -73,7 +77,7 @@ async function run(ctx, next, control_type=cfg_app.controller_folder) {
73
77
 
74
78
  if (ctx.ACTION[0] == '_' || ctx.ACTION[0] == '$' || typeof $controller[action] != 'function') {
75
79
  if(cfg_app.app_debug) {
76
- throw new Error(`class方法:${ctx.APP}/${control_type}/${ctx.CONTROLLER}/${humpAction}不存在!`);
80
+ throw new Error(`RunError: class方法:${ctx.APP}/${control_type}/${ctx.CONTROLLER}/${humpAction}不存在!`);
77
81
  } else {
78
82
  return false;
79
83
  }
@@ -93,18 +97,18 @@ async function run(ctx, next, control_type=cfg_app.controller_folder) {
93
97
  return stack.concat(async (ctx, next) => {
94
98
  const [METHOD = action, MIDDLEWARE = ctx.CONTROLLER, APP = ctx.APP] = middle.middleware.split('/').reverse();
95
99
  if(!ctx._[APP]) {
96
- throw new Error(`中间件应用:${APP}不存在!`);
100
+ throw new Error(`RunError: 中间件应用:${APP}不存在!`);
97
101
  }
98
102
  if(!ctx._[APP]['middleware']) {
99
- throw new Error(`中间件目录:${APP}/middleware不存在!`);
103
+ throw new Error(`RunError: 中间件目录:${APP}/middleware不存在!`);
100
104
  }
101
105
  const Middleware = ctx._[APP]['middleware'][MIDDLEWARE];
102
106
  if(!Middleware || typeof Middleware != 'function') {
103
- throw new Error(`中间件文件:${APP}/middleware/${MIDDLEWARE}不存在!`);
107
+ throw new Error(`RunError: 中间件文件:${APP}/middleware/${MIDDLEWARE}不存在!`);
104
108
  }
105
109
  const $middleware = new Middleware(ctx, next);
106
110
  if(!$middleware[METHOD] || typeof $middleware[METHOD] != 'function') {
107
- throw new Error(`中间件方法:${APP}/middleware/${MIDDLEWARE}/${METHOD}不存在!`);
111
+ throw new Error(`RunError: 中间件方法:${APP}/middleware/${MIDDLEWARE}/${METHOD}不存在!`);
108
112
  }
109
113
 
110
114
  // 执行中间件方法
package/lib/url.js CHANGED
@@ -12,7 +12,7 @@ const {toLine} = require('./utils/str');
12
12
  class Url extends Context
13
13
  {
14
14
  build(url='', vars, ext='', domain='') {
15
- if(typeof vars != 'object') {
15
+ if(vars && typeof vars != 'object') {
16
16
  [vars, ext, domain] = [{}, vars, ext];
17
17
  }
18
18
  const query = {...vars};
@@ -56,6 +56,7 @@ class Url extends Context
56
56
 
57
57
  if(~url.indexOf(':')) {
58
58
  url = url.replace(/\:([^./]+)/g, (match, key) => {
59
+ key = key.split('(')[0];
59
60
  if(key in query) {
60
61
  const value = query[key];
61
62
  delete query[key];
@@ -1,29 +1,33 @@
1
+ const fs = require('./fs.js');
2
+
1
3
  function parseError(err) {
2
- let stack = err.stack;
3
- stack = stack.replace(/(\r\n)|(\n)/g, '#split#');
4
- stack = stack.split('#split#');
5
- let msg = err.name;
4
+ let msg = err.message;
5
+ let stack = err.stack.replace(/(\r\n)/g, "\n").split("\n");
6
+
6
7
  if(err.name == 'TemplateError') {
7
- msg = stack.pop();
8
+ msg = 'TemplateError: ' + stack.pop();
9
+ stack = [stack.shift().replace('TemplateError:', "at")];
8
10
  } else {
9
- msg = stack.shift();
11
+ stack = stack.filter(text => {return /^ at /.test(text);});
10
12
  }
11
- let file_info = stack[0].split(' ').slice(-1)[0].replace(/(\()|(\))/g, '').split(':');
13
+
14
+ const file_info = stack[0].split(' ').slice(-1)[0].replace(/(\()|(\))/g, '').split(':');
12
15
  const column = parseInt(file_info.pop());
13
16
  const row = parseInt(file_info.pop());
14
17
  const file_path = file_info.join(':');
15
18
  let begin = row - 10;
16
19
  let end = row + 10;
20
+ let nth = 0;
17
21
  let code = '';
18
22
  if(begin < 0) {
19
23
  end -= begin;
20
24
  begin = 0;
21
25
  }
22
- if(file_path == 'anonymous') {
26
+ if(file_path == 'anonymous' || !file_path || !fs.isFileSync(file_path)) {
23
27
  code = stack;
24
28
  } else {
25
29
  code = require('fs').readFileSync(file_path);
26
- code = code.toString().split("\n");
30
+ code = code.toString().replace(/(\r\n)/g, "\n").split("\n");
27
31
  if(begin < 0) {
28
32
  end -= begin;
29
33
  begin = 0;
@@ -36,9 +40,6 @@ function parseError(err) {
36
40
  begin = 0;
37
41
  }
38
42
  code = code.slice(begin, end);
39
- if(err.name == 'TemplateError') {
40
- stack = [stack[0]];
41
- }
42
43
  nth = row - begin;
43
44
  }
44
45
 
package/lib/view.js CHANGED
@@ -71,7 +71,7 @@ class View extends Context
71
71
  }
72
72
  if(!is_file) {
73
73
  file_name = this.parsePath(template, this._folder, this._depr);
74
- throw new Error(`文件不存在(${file_name})`);
74
+ throw new Error(`ViewError: 文件不存在(${file_name})`);
75
75
  }
76
76
  return file_name;
77
77
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jj.js",
3
- "version": "0.8.3",
3
+ "version": "0.8.6",
4
4
  "description": "A simple and lightweight MVC framework built on nodejs+koa2",
5
5
  "main": "jj.js",
6
6
  "scripts": {