A super simple lightweight NodeJS MVC framework(一个超级简单轻量的NodeJS MVC框架)
无论什么语言什么框架的程序,类库的使用都离不开require、import、include等导入模块,不用时,还得删除。而本框架基于proxy实现了类库自动加载、懒加载和Class自动实例化及单例化技术,所有的类库,想用直接调用,系统会自动导入。
- 系统架构为经典的MVC模式,模仿Thinkphp5,很容易上手
- 系统类库、用户类库都支持自动加载、懒加载、Class自动生成单实例
- 支持应用级、路由级、控制器级三级中间件,方便插件及二次开发
- 支持单应用和多应用两种运行模式
- 基于jsdoc,提供完整的代码提示,自动生成应用端types类型文件
项目地址:https://github.com/yafoo/jj.js
码云镜像:https://gitee.com/yafu/jj.js
官网地址:https://me.i-i.me/special/jj.html
npm i jj.js
运行环境要求:node.js >= v12.7.0
1、创建控制器文件 ./app/controller/index.js
const {Controller} = require('jj.js');
class Index extends Controller
{
async index() {
this.$show('Hello jj.js, hello world !');
}
}
module.exports = Index;
2、创建应用入口文件 ./server.js
const {App, Logger} = require('jj.js');
const app = new App();
app.run(3000, '0.0.0.0', function(err){
!err && Logger.log('app', 'http server is ready on 3000');
});
3、运行命令
node server.js
4、浏览器访问 http://127.0.0.1:3000
,页面输出 Hello jj.js, hello world !
5、在Stackblitz中打开 Hello world !
├── app //应用目录 (非必需,可改名)
│ ├── controller //控制器目录 (非必需,可改名)
│ │ └── index.js //控制器
│ ├── view //模板目录 (非必需,可改名)
│ │ └── index //index控制器模板目录 (非必需,可改名)
│ │ └── index.htm //模板
│ ├── middleware //中间件目录 (非必需,不可改名)
│ ├── model //模型目录 (非必需,可改名)
│ ├── pagination //分页目录 (非必需,可改名)
│ ├── logic //逻辑目录 (非必需,可改名)
│ └── **** //其他目录 (非必需,可改名)
├── app2 //应用2目录 (非必需,可改名)
├── common //公共应用目录 (非必需,可改名)
├── config //配置目录 (非必需,不可改名)
│ ├── app.js //APP配置 (非必需,不可改名)
│ ├── db.js //数据库配置 (非必需,不可改名)
│ ├── routes.js //路由配置 (非必需,不可改名)
│ ├── view.js //模版配置 (非必需,不可改名)
│ ├── cache.js //缓存配置 (非必需,不可改名)
│ └── **** //其他配置 (非必需,可改名)
├── public //静态访问目录 (非必需,可改名)
│ └── static //css image文件目录 (非必需,可改名)
├── node_modules //nodejs模块目录
├── server.js //应用入口文件 (必需,可改名)
└── package.json //npm package.json
- config: 应用配置目录,系统的所有配置参数都放这里,也可以简化为一个config .js文件,所有配置参数会覆盖框架默认参数。其中config这个名字不允许修改。
- app、app2、common: 为应用目录,common为公共应用目录,可以存放公共的model、logic或其他的文件,app为默认访问的应用。jj.js框架支持单应用和多应用运行模式,在
./config/app.js
中把app_multi
设置为true
即为多应用模式,此时可以创建app2、app3更多应用。框架默认为单应用模式。 - public:静态资源目录,主要存放css文件、js文件、图片等静态资源。在
./config/app.js
中通过static_dir
参数可以设置或更改静态目录名字,为空时,则关闭静态访问(系统不会加载静态资源访问的逻辑,最大节省内容,也即所谓的轻量) - server.js:应用入口文件,名字可以任意改。
const {App, Controller, Db, Model, Pagination, View, Logger, Cookie, Response, Upload, Url, Middleware, Cache, Context, View} = require('jj.js');
系统类库都是Class
类型,其中Logger
和Cache
是静态类;开发时建议继承系统类库,这样可以在类内使用$
开头的属性,实现自动加载功能,链式调用类方法时会自动实例化一个单例。例如,在控制器内使用 this.$logger
会返回系统Logger类,使用 this.$logger.info()
,会自动生成一个logger
单例,并调用info
方法,其他系统类库及类库内可以以这种方法调用。
类库自动加载、懒加载是整个框架的核心,这里以hello world!示例程序先简单做个介绍。
1、假如想在./app/controller/index.js
控制器的user
方法中读取user
数据表中id
为1的用户,我们直接上代码:
const {Controller} = require('jj.js');
class Index extends Controller
{
async index() {
this.$show('Hello jj.js, hello world !');
}
async user() {
const user_info = await this.$db.table('user').find({id: 1});
this.$show(user_info);
}
}
module.exports = Index;
-
访问url:
http://127.0.0.1:3000/index/user
,即可看到打印的json用户信息。 -
其中
async user() {}
为异步方法,在控制器中,对外可以以url访问的方法必须设置为异步函数。使用this.$db
调用框架db类,这里控制器必须继承框架Controller
类,否则会调用失败。 -
在控制器中,前缀为
$
字符的属性为特殊属性,框架首先会检测本类中即this
实例中是否有$db
属性,有的话,会直接调用。如果没有,会检测本类文件所在应用目录即./app/
目录下,是否用db
目录或文件,如果有,则$db
即代表那个目录或文件,this.$db.table
会继续在db目录下寻找table目录或文件。如果db
目录或文件不存在,框架会在应用根目录./
下找,是否有db
目录或文件,如果有,和上面一样。如果还不存在,框架会在jj.js框架lib
目录下找db
文件或目录,而框架lib
中有db.js
文件,至此,this.$db
成功访问到框架db
类。 -
如果将
this.$db
赋值为给一个变量,const db = this.$db;
,得到的将是一个db
Class类,可以进行new操作,const db = this.$db; const db1 = new db(this.ctx, options); const db2 = new this.$db(this.ctx, options);
创建多个实例。 -
但上面演示代码并没有new一个实例,而是直接调用
db
类的table()
方法,这正是框架的智能之处。当调用this.$db.table('user')
时,框架首先会检测db类是否有table
静态属性,有的话会直接调用。没有,则会自动new一个db
实例,而且这个实例是个单例,然后调用此db
实例的table
方法,table('user')
设置数据表名后返回实例本身,然后紧接着调用find()
方法,读取用户ID为1的数据。
注意:虽然系统加载很复杂, 但是基于node的常驻内存特性,上面的文件路径判断,处理一次就会被缓存下来,在下个生命周期不用重复判断,节省性能。
关于单例:通过
this.$xxx
调用的类实例,在单个生命周期内是单例,即不管调用几次,都是用的同一个实例,这样非常节省内存开销。如果有多个db实例需求,可以用上面的方法,自己new
创建,自己创建时注意第一个参数需传入ctx,否则部分类库使用会出现异常。
生命周期:应用生命周期以url访问为单位,一次url访问到访问结束,是一个独立的生命周期,在这个周期内自动生成的单实例都是共用的。
2、假如自己创建了数据表模型./app/model/user.js
,然后想在控制器中使用,模型文件代码如下:
const {Model} = require('jj.js');
class User extends Model
{
async getUserInfo(condition) {
const user_info = await this.db.find(condition);
return user_info;
}
}
module.exports = User;
这时在控制器的user方法中读取user数据表中id为1的用户,我们直接上代码:
const {Controller} = require('jj.js');
class Index extends Controller
{
async index() {
this.$show('Hello jj.js, hello world !');
}
async user() {
const user_info = await this.$model.user.getUserInfo({id: 1});
this.$show(user_info);
}
}
module.exports = Index;
-
访问url:
http://127.0.0.1:3000/index/user
,同样看到打印的json用户信息。 -
根据示例1的加载检测机制,
this.$model
会自动定位到./app/model/
目录,this.$model.user
会自动加载./app/model/user.js
类文件,调用方法getUserInfo
,会自动生成一个user
模型的单实例,并调此单实例的getUserInfo
方法。 -
可以看到,不管是系统类库,还是自定义类库,都可以实现自动加载、懒加载、自动生成单实例。
注意:在
user
模型内调用的是this.db
,db
没有$
前缀,这个db
是专属于user
模型的独立实例,与示例1生成的db
实例不是同一个。当然也可以使用带$
前缀的db
,但这样,在一个生命周期同时调用多个不同的模型的话,容易造成混淆。
3、除了框架db类、自定义模型类,整个应用和框架的所有自定义类库、配置文件,都支持同过this.$xxxx
调用。
应用的配置可以为./config/
目录+./config/xxx.js
文件的形式,也可以直接写到一个./config.js
文件里。
应用配置不用每项都设置,只设置自己需要改的,默认会继承框架的默认配置,在控制器、中间件、模板类、模型类里都可以通过this.$config.xxx
使用。
- app配置
./config/app.js
:
const app = {
app_debug: true, // 调试模式
app_multi: false, // 是否开启多应用
default_app: 'app', // 默认应用
default_controller: 'index', //默认控制器
default_action: 'index', // 默认方法
common_app: 'common', // 公共应用,存放公共模型及逻辑
controller_folder: 'controller', //控制器目录名
static_dir: '', // 静态文件目录,相对于应用根目录,为空或false时,关闭静态访问
koa_body: null // koa-body配置参数,为''、null、false时,关闭koa-body,此时upload类将不可用,并且系统接收不到post参数。启用的话,建议配置:{multipart: true, formidable: {keepExtensions: true, maxFieldsSize: 10 * 1024 * 1024}}
}
module.exports = app;
- 数据库配置
./config/db.js
:可以配置多个,方便程序里切换使用。
const db = {
default: {
type : 'mysql', // 数据库类型
host : '127.0.0.1', // 服务器地址
database : 'jj', // 数据库名
user : 'root', // 数据库用户名
password : '', // 数据库密码
port : '', // 数据库连接端口
charset : 'utf8', // 数据库编码默认采用utf8
prefix : 'jj_' // 数据库表前缀
}
}
module.exports = db;
- 路由配置
./config/routes.js
: 路由功能基于@koa/router
开发,关于url匹配规则可以参考官方文档:文档地址
本框架默认内置
应用/控制器/方法
的全局路由,即如果不需要定制url,可以直接访问,无需配置路由。
路由配置为一个数组,每一项为一条规则,项属性包含url规则,path访问路径,name规则名字,type路由类型。url一旦匹配,即会停止,如果需要继续向下匹配,需在程序内调用
await this.$next();
,路由配置参考示例:
routes = [
{url: '/', path: 'app/index/index2'}, // 访问'/',会定位到app应用的index控制的index2方法
{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'
{url: '/admin', path: 'app/admin/check', type: 'middleware'}, // 访问'/admin',会定位到app应用的admin中间件的check方法
{url: '/about', path: 'app/about/index', type: 'view'}, // 访问'/about',会定位到app应用的view模板目录about目录下的index.htm文件,并直接输出文件内容
{url: '/:cate/list_:page.html', path: 'cate/cate', name: 'cate_page'}, // 多参数分页示例
{url: '/hello', path: async (ctx, next) => {ctx.body = 'hello world, hello jj.js!';}} // 直接路由到函数
];
module.exports = routes;
注意:本路由配置示例前两条规则为常规用法,后面的非常规用法,不建议使用。如果是单应用模式,可以去掉path参数里的app。
- 自定义配置
./config/self.js
:自定义配置同样可以直接通过this.$config.self
使用。
const self = {
option1: ''
option2: ''
...
}
module.exports = self;
- 其他配置项,请参考系统配置文件
jj.js/lib/config.js
类名使用大驼峰,方法名使用小驼峰,私有方法使用下划线前缀。 控制器文件名使用小写下划线。
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}