jj.js 1.0.0-rc.2 → 1.0.0-rc.4
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 +16 -2
- package/bin/jj.js +132 -0
- package/lib/controller.js +2 -2
- package/lib/mvc.js +2 -2
- package/lib/request.js +2 -2
- package/package.json +4 -1
- package/templates/hello/app/controller/index.js +9 -0
- package/templates/hello/config/app.js +8 -0
- package/templates/hello/jsconfig.json +8 -0
- package/templates/hello/package.json +12 -0
- package/templates/hello/server.js +8 -0
- package/templates/todo/app/controller/index.js +9 -0
- package/templates/todo/app/controller/todo.js +45 -0
- package/templates/todo/app/view/todo/index.htm +55 -0
- package/templates/todo/config/app.js +9 -0
- package/templates/todo/config/db.js +17 -0
- package/templates/todo/jsconfig.json +8 -0
- package/templates/todo/package.json +12 -0
- package/templates/todo/server.js +8 -0
package/README.md
CHANGED
|
@@ -37,9 +37,23 @@ jj.js 是一个模仿 ThinkPHP5 设计的轻量级 Node.js MVC 框架。基于 P
|
|
|
37
37
|
npm i jj.js
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
> **运行环境要求**:Node.js >=
|
|
40
|
+
> **运行环境要求**:Node.js >= 20.19.0
|
|
41
41
|
|
|
42
|
-
###
|
|
42
|
+
### 方式一:CLI 初始化(推荐)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# 创建项目(交互式选择模板)
|
|
46
|
+
npx jj.js init myapp
|
|
47
|
+
|
|
48
|
+
# 进入项目并启动
|
|
49
|
+
cd myapp
|
|
50
|
+
npm install
|
|
51
|
+
npm start
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 方式二:手动创建
|
|
55
|
+
|
|
56
|
+
#### Hello World
|
|
43
57
|
|
|
44
58
|
**1. 创建控制器** `./app/controller/index.js`
|
|
45
59
|
|
package/bin/jj.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
|
|
7
|
+
const templates = {
|
|
8
|
+
hello: 'Hello World 示例项目',
|
|
9
|
+
todo: 'Todo List 完整示例项目'
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function showHelp() {
|
|
13
|
+
console.log(`
|
|
14
|
+
jj.js - 轻量级 Node.js MVC 框架
|
|
15
|
+
|
|
16
|
+
用法:
|
|
17
|
+
npx jj.js init <project-name>
|
|
18
|
+
|
|
19
|
+
示例:
|
|
20
|
+
npx jj.js init myapp
|
|
21
|
+
`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function selectTemplate() {
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
const rl = readline.createInterface({
|
|
27
|
+
input: process.stdin,
|
|
28
|
+
output: process.stdout
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
console.log('\n请选择项目模板:');
|
|
32
|
+
const templateList = Object.entries(templates);
|
|
33
|
+
templateList.forEach(([name, desc], index) => {
|
|
34
|
+
console.log(` ${index + 1}. ${name.padEnd(10)} - ${desc}`);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
rl.question('\n请输入序号 (1): ', (answer) => {
|
|
38
|
+
rl.close();
|
|
39
|
+
const index = parseInt(answer) - 1;
|
|
40
|
+
if (index >= 0 && index < templateList.length) {
|
|
41
|
+
resolve(templateList[index][0]);
|
|
42
|
+
} else {
|
|
43
|
+
resolve('hello'); // 默认选择第一个
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function copyDir(src, dest) {
|
|
50
|
+
if (!fs.existsSync(dest)) {
|
|
51
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
55
|
+
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
const srcPath = path.join(src, entry.name);
|
|
58
|
+
const destPath = path.join(dest, entry.name);
|
|
59
|
+
|
|
60
|
+
if (entry.isDirectory()) {
|
|
61
|
+
copyDir(srcPath, destPath);
|
|
62
|
+
} else {
|
|
63
|
+
fs.copyFileSync(srcPath, destPath);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function initProject(template, projectName) {
|
|
69
|
+
if (!templates[template]) {
|
|
70
|
+
console.error(`错误: 未知的模板 "${template}"`);
|
|
71
|
+
console.log('\n可用模板:');
|
|
72
|
+
for (const [name, desc] of Object.entries(templates)) {
|
|
73
|
+
console.log(` ${name.padEnd(10)} ${desc}`);
|
|
74
|
+
}
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const projectPath = path.resolve(process.cwd(), projectName || template + '-app');
|
|
79
|
+
|
|
80
|
+
if (fs.existsSync(projectPath)) {
|
|
81
|
+
console.error(`错误: 目录 "${projectName}" 已存在`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log(`正在创建 ${templates[template]}...`);
|
|
86
|
+
|
|
87
|
+
// 复制模板文件
|
|
88
|
+
const templatePath = path.join(__dirname, '..', 'templates', template);
|
|
89
|
+
copyDir(templatePath, projectPath);
|
|
90
|
+
|
|
91
|
+
// 更新 package.json 中的项目名称
|
|
92
|
+
const pkgPath = path.join(projectPath, 'package.json');
|
|
93
|
+
if (fs.existsSync(pkgPath)) {
|
|
94
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
95
|
+
pkg.name = projectName || template + '-app';
|
|
96
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log(`\n✓ 项目创建成功: ${projectPath}`);
|
|
100
|
+
console.log('\n下一步:');
|
|
101
|
+
console.log(` cd ${projectName || template + '-app'}`);
|
|
102
|
+
console.log(' npm install');
|
|
103
|
+
console.log(' npm start');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 解析命令行参数
|
|
107
|
+
const args = process.argv.slice(2);
|
|
108
|
+
const command = args[0];
|
|
109
|
+
|
|
110
|
+
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
111
|
+
showHelp();
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (command === 'init') {
|
|
116
|
+
const projectName = args[1];
|
|
117
|
+
|
|
118
|
+
if (!projectName) {
|
|
119
|
+
console.error('错误: 请指定项目名称');
|
|
120
|
+
showHelp();
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 交互式选择模板
|
|
125
|
+
selectTemplate().then(template => {
|
|
126
|
+
initProject(template, projectName);
|
|
127
|
+
});
|
|
128
|
+
} else {
|
|
129
|
+
console.error(`错误: 未知的命令 "${command}"`);
|
|
130
|
+
showHelp();
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
package/lib/controller.js
CHANGED
|
@@ -14,12 +14,12 @@ class Controller extends Middleware
|
|
|
14
14
|
/**
|
|
15
15
|
* 初始化方法,在控制器方法执行前自动执行
|
|
16
16
|
* @protected
|
|
17
|
-
* @returns {any} -
|
|
17
|
+
* @returns {('__EXIT__'|any)} - 如果返回__EXIT__,则不会执行控制器及后续方法
|
|
18
18
|
*/
|
|
19
19
|
_init() {}
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* 结束方法,在控制器方法执行后自动执行。如果控制器方法返回
|
|
22
|
+
* 结束方法,在控制器方法执行后自动执行。如果控制器方法返回__EXIT__,则不会执行此方法
|
|
23
23
|
* @protected
|
|
24
24
|
*/
|
|
25
25
|
_end() {}
|
package/lib/mvc.js
CHANGED
|
@@ -106,8 +106,8 @@ async function mvc(ctx, next) {
|
|
|
106
106
|
await compose(middlewares)(ctx, async () => {
|
|
107
107
|
let result;
|
|
108
108
|
typeof $controller['_init'] == 'function' && (result = await $controller['_init']());
|
|
109
|
-
result !==
|
|
110
|
-
result !==
|
|
109
|
+
result !== '__EXIT__' && (result = await $controller[action]());
|
|
110
|
+
result !== '__EXIT__' && typeof $controller['_end'] == 'function' && await $controller['_end']();
|
|
111
111
|
});
|
|
112
112
|
}
|
|
113
113
|
|
package/lib/request.js
CHANGED
|
@@ -194,7 +194,7 @@ class Request extends Context
|
|
|
194
194
|
* @returns {*}
|
|
195
195
|
*/
|
|
196
196
|
header(key, default_value='') {
|
|
197
|
-
return this.ctx.headers[key]
|
|
197
|
+
return this._getValue(this.ctx.headers[key], default_value);
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
/**
|
|
@@ -212,7 +212,7 @@ class Request extends Context
|
|
|
212
212
|
* @returns {*}
|
|
213
213
|
*/
|
|
214
214
|
_getValue(value, default_value) {
|
|
215
|
-
return value === undefined ? default_value : typeof
|
|
215
|
+
return value === undefined ? default_value : typeof default_value === 'number' ? Number(value) : value;
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jj.js",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.4",
|
|
4
4
|
"description": "A super simple lightweight NodeJS MVC framework(一个超级简单轻量的NodeJS MVC框架)",
|
|
5
5
|
"main": "jj.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"jj-cli": "./bin/jj.js"
|
|
8
|
+
},
|
|
6
9
|
"scripts": {
|
|
7
10
|
"demo": "node ./tests/_demo/server.js",
|
|
8
11
|
"test": "node --test"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const { Controller } = require('jj.js');
|
|
2
|
+
|
|
3
|
+
class TodoController extends Controller {
|
|
4
|
+
async index() {
|
|
5
|
+
const db = this.$db;
|
|
6
|
+
// 确保表存在
|
|
7
|
+
await db.execute(`
|
|
8
|
+
CREATE TABLE IF NOT EXISTS todo_todo (
|
|
9
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
10
|
+
title TEXT NOT NULL DEFAULT '',
|
|
11
|
+
completed INTEGER NOT NULL DEFAULT 0,
|
|
12
|
+
add_time INTEGER NOT NULL DEFAULT 0
|
|
13
|
+
)
|
|
14
|
+
`);
|
|
15
|
+
const todos = await db.table('todo').order('id', 'desc').select();
|
|
16
|
+
this.$assign('todos', todos);
|
|
17
|
+
await this.$fetch('todo/index');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async add() {
|
|
21
|
+
const title = this.$request.post('title');
|
|
22
|
+
if (title) {
|
|
23
|
+
const db = this.$db;
|
|
24
|
+
await db.table('todo').insert({ title, completed: 0, add_time: Date.now() });
|
|
25
|
+
}
|
|
26
|
+
this.$redirect('/todo');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async toggle() {
|
|
30
|
+
const id = this.$request.query('id');
|
|
31
|
+
const completed = this.$request.query('completed', 0);
|
|
32
|
+
const db = this.$db;
|
|
33
|
+
await db.table('todo').where({ id }).update({ completed: completed ? 0 : 1 });
|
|
34
|
+
this.$redirect('/todo');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async delete() {
|
|
38
|
+
const id = this.$request.query('id');
|
|
39
|
+
const db = this.$db;
|
|
40
|
+
await db.table('todo').where({ id }).delete();
|
|
41
|
+
this.$redirect('/todo');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = TodoController;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Todo List - jj.js</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f5f5f5; padding: 20px; }
|
|
10
|
+
.container { max-width: 600px; margin: 0 auto; }
|
|
11
|
+
h1 { text-align: center; color: #333; margin-bottom: 30px; }
|
|
12
|
+
.add-form { display: flex; gap: 10px; margin-bottom: 20px; }
|
|
13
|
+
.add-form input { flex: 1; padding: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 16px; }
|
|
14
|
+
.add-form button { padding: 12px 24px; background: #4CAF50; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 16px; }
|
|
15
|
+
.add-form button:hover { background: #45a049; }
|
|
16
|
+
.todo-list { list-style: none; }
|
|
17
|
+
.todo-item { display: flex; align-items: center; gap: 12px; padding: 15px; background: white; border-radius: 8px; margin-bottom: 10px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
|
18
|
+
.todo-item.completed .todo-text { text-decoration: line-through; color: #999; }
|
|
19
|
+
.todo-text { flex: 1; font-size: 16px; }
|
|
20
|
+
.todo-actions { display: flex; gap: 8px; }
|
|
21
|
+
.btn { padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; }
|
|
22
|
+
.btn-toggle { background: #2196F3; color: white; }
|
|
23
|
+
.btn-delete { background: #f44336; color: white; }
|
|
24
|
+
.empty { text-align: center; color: #999; padding: 40px; }
|
|
25
|
+
</style>
|
|
26
|
+
</head>
|
|
27
|
+
<body>
|
|
28
|
+
<div class="container">
|
|
29
|
+
<h1>📝 Todo List</h1>
|
|
30
|
+
|
|
31
|
+
<form class="add-form" action="/todo/add" method="POST">
|
|
32
|
+
<input type="text" name="title" placeholder="添加新任务..." required>
|
|
33
|
+
<button type="submit">添加</button>
|
|
34
|
+
</form>
|
|
35
|
+
|
|
36
|
+
{{if todos.length === 0}}
|
|
37
|
+
<div class="empty">暂无任务,添加一个吧!</div>
|
|
38
|
+
{{else}}
|
|
39
|
+
<ul class="todo-list">
|
|
40
|
+
{{each todos todo}}
|
|
41
|
+
<li class="todo-item {{if todo.completed}}completed{{/if}}">
|
|
42
|
+
<span class="todo-text">{{todo.title}}</span>
|
|
43
|
+
<div class="todo-actions">
|
|
44
|
+
<a href="/todo/toggle?id={{todo.id}}&completed={{todo.completed}}" class="btn btn-toggle">
|
|
45
|
+
{{if todo.completed}}撤销{{else}}完成{{/if}}
|
|
46
|
+
</a>
|
|
47
|
+
<a href="/todo/delete?id={{todo.id}}" class="btn btn-delete" onclick="return confirm('确定删除吗?')">删除</a>
|
|
48
|
+
</div>
|
|
49
|
+
</li>
|
|
50
|
+
{{/each}}
|
|
51
|
+
</ul>
|
|
52
|
+
{{/if}}
|
|
53
|
+
</div>
|
|
54
|
+
</body>
|
|
55
|
+
</html>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
|
|
4
|
+
const dataDir = path.join(__dirname, '../data');
|
|
5
|
+
if(!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @module config
|
|
9
|
+
* @type {import('jj.js').DbConfig}
|
|
10
|
+
*/
|
|
11
|
+
module.exports = {
|
|
12
|
+
default: {
|
|
13
|
+
type: 'sqlite',
|
|
14
|
+
database: path.join(dataDir, 'todo.db'),
|
|
15
|
+
prefix: 'todo_'
|
|
16
|
+
}
|
|
17
|
+
};
|