vboot 0.0.1

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.
File without changes
package/core/index.js ADDED
@@ -0,0 +1,739 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const express = require("express");
4
+ const core = require("./lib/core.js");
5
+ // node接入证书
6
+ var https = require('https');
7
+ // 深度合并
8
+ const deepmerge = require('deepmerge');
9
+ // swagger
10
+ const swaggerJSDoc = require('swagger-jsdoc');
11
+ const swaggerUi = require('swagger-ui-express');
12
+ const j2s = require('joi-to-swagger');
13
+ const joi = require('joi');
14
+ const payloadValidate = require('./middlewares/payloadValidate.js');
15
+ const utils = require('./utils');
16
+ const { pathToRegexp } = require('path-to-regexp');
17
+
18
+ // 增加缓存机制
19
+ const fileCache = new Map();
20
+
21
+ // 插件系统
22
+ class PluginManager {
23
+ register(hookName, fn) {
24
+ this.hooks[hookName] = this.hooks[hookName] || [];
25
+ this.hooks[hookName].push(fn);
26
+ }
27
+
28
+ async trigger(hookName, ...args) {
29
+ return Promise.all(this.hooks[hookName].map(fn => fn(...args)));
30
+ }
31
+ }
32
+
33
+ function isArrayOfStringsAndRegExp(arr) {
34
+ return Array.isArray(arr) && arr.every(item => typeof item === 'string' || item instanceof RegExp);
35
+ }
36
+ /**
37
+ * 拼接路径前缀和正则表达式路径,生成一个新的正则表达式
38
+ * @param {string} pathPrefix - 路径前缀字符串,例如 '/api/novel'
39
+ * @param {RegExp} pathRegex - 正则表达式路径,例如 /\/books\/\d+/
40
+ * @returns {RegExp} 拼接后的新正则表达式
41
+ */
42
+ function combinePath(pathPrefix, path) {
43
+ if (!pathPrefix) {
44
+ return path;
45
+ }
46
+ if (typeof path === 'string') {
47
+ return pathPrefix + path;
48
+ }
49
+ // 转义路径前缀中的特殊字符,使其适配正则表达式
50
+ const escapedPrefix = pathPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
51
+
52
+ // 拼接前缀和正则表达式的源码部分
53
+ let pathNew = path.source.indexOf('^') === 0 ? path.source.substring(1) : path.source;
54
+ const combinedSource = escapedPrefix + pathNew;
55
+
56
+ // 创建新的正则表达式,继承原正则的标志(如 'i'、'g' 等)
57
+ return new RegExp(combinedSource, path.flags);
58
+ }
59
+
60
+ /**
61
+ * @description 基于express封装的mvc框架,遵循约定优于配置原则
62
+ */
63
+ class VBoot {
64
+ // 添加静态实例属性
65
+ static _instance = null;
66
+ // 静态获取实例方法
67
+ static create(options = {}) {
68
+ if (VBoot._instance) {
69
+ throw new Error('实例已经存在,请使用 VBoot.getInstance() 获取实例');
70
+ }
71
+ VBoot._instance = new VBoot(options);
72
+ return VBoot._instance;
73
+ }
74
+ // 静态获取实例方法
75
+ static getInstance() {
76
+ if (!VBoot._instance) {
77
+ throw new Error('实例不存在,请使用 VBoot.create() 创建实例');
78
+ }
79
+ return VBoot._instance;
80
+ }
81
+ // 创建应用实例(工厂方法)
82
+ static createApp(options = {}) {
83
+ return new VBoot(options);
84
+ }
85
+ // 配置初始化
86
+ initConfig(customConfig) {
87
+ const baseConfig = require('./lib/config');
88
+ return deepmerge.all([
89
+ baseConfig,
90
+ customConfig || {}
91
+ ]);
92
+ }
93
+ constructor(options = {}) {
94
+ console.log('VBoot starting...');
95
+ //守卫(局部)中间件
96
+ this.guards = {};
97
+ // 扩展
98
+ this.extends = {};
99
+ // services
100
+ this.services = {};
101
+ this.utils = {};
102
+ this.plugins = new PluginManager();
103
+ // Express实例
104
+ this.app = express();
105
+ this.app.vithingInstance = this; // 反向引用
106
+ // 路由系统
107
+ this.router = express.Router();//路由实例
108
+ this.routers = [];//路由数据
109
+ // 配置系统
110
+ this.config = this.initConfig(options.config);
111
+ this.beforeStart = options.beforeStart || null;
112
+ this.beforeLoadRouter = options.beforeLoadRouter || null;
113
+
114
+ // 在Vithing类constructor中添加
115
+ this.swaggerSpec = swaggerJSDoc({
116
+ definition: {
117
+ openapi: '3.0.0',
118
+ info: {
119
+ title: 'API Documentation',//TODO API系统名称
120
+ version: '1.0.0',
121
+ }
122
+ },
123
+ apis: [] // 动态生成
124
+ });
125
+ }
126
+ // 加载本地配置
127
+ async loadLocalConfig() {
128
+ const configPath = path.join(this.config.SRC_PATH, "config/index.js");
129
+ if (fs.existsSync(configPath)) {
130
+ const localConfig = require(configPath);
131
+ // 合并本地配置
132
+ this.config = deepmerge(this.config, localConfig);
133
+ }
134
+ console.log('============本地配置加载完成');
135
+ }
136
+ /**
137
+ * @description app核心模块:日志、favicon 图标、cookie、json、url、模板引擎、静态资源
138
+ */
139
+ loadCore() {
140
+ core(this.app, this.config);
141
+ console.log('============核心模块加载完成');
142
+ }
143
+ // 加载扩展工具
144
+ async loadExtends() {
145
+ // TODO 先加载默认扩展
146
+
147
+ // 再加载外部扩展
148
+ const extendPath = path.join(this.config.SRC_PATH, "extends/index.js");
149
+
150
+ if (fs.existsSync(extendPath)) {
151
+ let extendList = require(extendPath);
152
+ for (let index = 0; index < extendList.length; index++) {
153
+ const extend = extendList[index];
154
+ if (extend.name && extend.init) {
155
+ // 挂载
156
+ const service = await extend.init(this.app, this.config);
157
+ this.extends[extend.name] = service;
158
+ }
159
+ }
160
+ }
161
+ console.log('============扩展工具加载完成');
162
+ }
163
+ /**
164
+ * 文件路径转模块名(去.js后缀)
165
+ *
166
+ * @param {any} rootDir 模块入口
167
+ * @param {any} postfix 需要的文件后缀
168
+ * @returns
169
+ */
170
+ scanDirModules(rootDir, postfix = '.js') {
171
+ if (fileCache.has(rootDir)) {
172
+ return fileCache.get(rootDir);
173
+ }
174
+ // 文件集合
175
+ const fileList = []
176
+ // 获取目录下的第一级子文件为路由文件队列
177
+ let filenames = fs.readdirSync(rootDir);
178
+ while (filenames.length) {
179
+ // 路由文件相对路径
180
+ const relativeFilePath = filenames.shift()
181
+ // 路由文件绝对路径
182
+ const absFilePath = path.join(rootDir, relativeFilePath);
183
+
184
+ if (fs.statSync(absFilePath).isDirectory()) {
185
+ // 是文件夹的情况下,读取子目录文件,添加到路由文件队列中
186
+ // const subFiles = fs.readdirSync(absFilePath).map(v => path.join(absFilePath.replace(rootDir, ''), v))
187
+ const relativePath = path.relative(rootDir, absFilePath);
188
+ const subFiles = fs.readdirSync(absFilePath).map(v => path.join(relativePath, v));
189
+
190
+ filenames = filenames.concat(subFiles)
191
+ } else {
192
+ // 是文件的情况下,先排除非文件
193
+ if (!absFilePath.endsWith(postfix)) {
194
+ continue
195
+ }
196
+ fileList.push(absFilePath);
197
+ }
198
+ }
199
+ // 加入缓存
200
+ fileCache.set(rootDir, fileList);
201
+ return fileList
202
+ }
203
+ /**
204
+ * 文件路径转对象结构
205
+ *
206
+ * @param {any} rootDir 模块入口
207
+ * @param {any} needLast 需不需要最后一级
208
+ * @returns
209
+ */
210
+ pathToNestedObject(path, requireValue, needLast = true) {
211
+ const parts = path.replace('.js', '').split('\\');
212
+ if (!needLast) {
213
+ parts.pop();
214
+ }
215
+ if (parts[0] === '') {
216
+ parts.shift();
217
+ }
218
+ // 递归构建对象
219
+ function buildObject(parts, value) {
220
+ if (parts.length === 0) {
221
+ return value;
222
+ }
223
+ const key = parts.shift();
224
+ return {
225
+ [key]: buildObject(parts, value)
226
+ };
227
+ }
228
+
229
+ // 调用递归函数
230
+ return buildObject(parts, requireValue);
231
+ }
232
+ /**
233
+ * @description: 加载中间件
234
+ * @param {*} node 属性节点
235
+ * @param {*} isGlobal 是否全局中间件
236
+ * @return {*}
237
+ */
238
+ async loadMiddlewares(node, isGlobal = true) {
239
+ const extendPath = path.join(this.config.SRC_PATH, 'middlewares', node, "index.js");
240
+ if (!fs.existsSync(extendPath)) {
241
+ console.warn(extendPath + '未配置中间件');
242
+ return;
243
+ }
244
+ let middlewares;
245
+ try {
246
+ middlewares = require(extendPath);
247
+ } catch (error) {
248
+ console.error('中间件加载失败:', error);
249
+ return;
250
+ }
251
+ const list = Array.isArray(middlewares) ? middlewares : [];
252
+
253
+ for (const ware of list) {
254
+ if (!ware) {
255
+ console.warn('无效的中间件配置:中间件不存在');
256
+ continue;
257
+ }
258
+ if (typeof ware.execute !== 'function') {
259
+ console.warn('无效的中间件配置:execute需要是一个方法');
260
+ continue;
261
+ }
262
+
263
+ if (isGlobal) {
264
+ const path = ware.path || '/';
265
+ this.app.use(path, ware.execute);
266
+ } else {
267
+ if (!ware.name) {
268
+ console.warn('❌ 守卫(局部)中间件缺少名称');
269
+ continue;
270
+ }
271
+ this.guards[ware.name] = ware.execute;
272
+ }
273
+ }
274
+ }
275
+ /**
276
+ * @description: 加载前置全局中间件
277
+ * @return {*}
278
+ */
279
+ async loadGlobalMiddlewares() {
280
+ this.loadMiddlewares('globalMiddlewares');
281
+ console.log('============全局中间件加载完成');
282
+ }
283
+ /**
284
+ * @description: 加载局部守卫中间件
285
+ * @return {*}
286
+ */
287
+ async loadGuardMiddlewares() {
288
+ this.loadMiddlewares('guardMiddlewares', false);
289
+ console.log('============局部守卫中间件加载完成');
290
+ }
291
+
292
+ /**
293
+ * @description: 加载模块路由
294
+ * @return {*}
295
+ */
296
+ async loadModules(dirList, isCommon) {
297
+ let fileList;
298
+ if (isCommon) {
299
+ fileList = dirList.filter((f) => {
300
+ return f === path.join(this.config.SRC_PATH, "modules", "controller.js");
301
+ })
302
+ } else {
303
+ fileList = dirList.filter((f) => {
304
+ return f !== path.join(this.config.SRC_PATH, "modules", "controller.js") && f.endsWith('\\controller.js');
305
+ })
306
+ }
307
+ await this.loadControllers(fileList);
308
+ }
309
+ /**
310
+ * @description 扫描模块下所有router.js
311
+ * @param {*} moduleDir 模块路径
312
+ * @param {*} moduleName 模块名称
313
+ */
314
+ async loadServices(modulesDir, fileList) {
315
+ let list = fileList.filter((f) => {
316
+ return f.endsWith('\\service.js');
317
+ })
318
+ for (let i = 0; i < list.length; i++) {
319
+ const dir = list[i];
320
+ // 模块名
321
+ let filePath = dir.replace(modulesDir, '');
322
+ const requireValue = require(dir);
323
+ let obj = this.pathToNestedObject(filePath, requireValue, false)
324
+ this.services = deepmerge(this.services, obj);
325
+ }
326
+ }
327
+ /**
328
+ * 将文件名修正为前缀
329
+ *
330
+ * @param {String} filename
331
+ * @returns {String}
332
+ */
333
+ transform(filename) {
334
+ return filename.slice(0, filename.lastIndexOf('.'))
335
+ // 分隔符转换
336
+ .replace(/\\/g, '/')
337
+ // index去除
338
+ .replace('/index', '/')
339
+ // 路径头部/修正
340
+ .replace(/^[/]*/, '/')
341
+ // 去除/controller
342
+ .replace('/controller', '')
343
+ // 路径尾部/去除
344
+ .replace(/[/]*$/, '')
345
+ }
346
+
347
+ /**
348
+ * @description 扫描controller.js
349
+ * @param {*} moduleDir 模块路径
350
+ * @param {*} moduleName 模块名称
351
+ */
352
+ async loadControllers(fileList) {
353
+ let apiRouteList = [];
354
+ const modulesDir = path.join(this.config.SRC_PATH, "modules");
355
+
356
+ for (let i = 0; i < fileList.length; i++) {
357
+ const dir = fileList[i];
358
+ // 模块名
359
+ let filePath = dir.replace(modulesDir, '');
360
+ let prefix = this.transform(filePath);
361
+
362
+ const dirRouterMap = require(dir);
363
+ for (let key in dirRouterMap) {
364
+ let options = dirRouterMap[key];
365
+ let valid = options && options.handler && Object.prototype.toString.call(options.handler) === '[object Function]' || Object.prototype.toString.call(options.handler) === '[object AsyncFunction]'
366
+ if (!valid) {
367
+ throw new Error(`API模块:${filePath}路由未成功导出`);
368
+ }
369
+
370
+ if (options.handler.length === 4) {
371
+ // 错误处理中间件
372
+ this.app.use(options.handler);
373
+ continue;
374
+ }
375
+
376
+ if (!options.path) {
377
+ if (options.method === 'use') {
378
+ options.pathPrefix = '';
379
+ options.path = '';
380
+ } else {
381
+ // 没传path默认用对应的属性名
382
+ options.pathPrefix = prefix;
383
+ options.path = `/${key}`;
384
+ }
385
+ } else {
386
+ if (options.isFullPath) {
387
+ options.pathPrefix = '';
388
+ } else {
389
+ options.pathPrefix = prefix;
390
+ }
391
+ }
392
+ if (!(typeof options.path === 'string' || isArrayOfStringsAndRegExp(options.path) || options.path instanceof RegExp)) {
393
+ console.log(options.path)
394
+ throw new Error('options.path 必须是字符串、正则、数组(字符串元素、正则元素)');
395
+ }
396
+ // 针对三种情况拼接完整的path
397
+ if (isArrayOfStringsAndRegExp(options.path)) {
398
+ options.path = options.path.map(item => {
399
+ return combinePath(options.pathPrefix, item);
400
+ });
401
+ } else if (options.path instanceof RegExp) {
402
+ // TODO 正则处理
403
+ options.path = combinePath(options.pathPrefix, options.path);
404
+ } else {
405
+ // string
406
+ options.path = options.pathPrefix + options.path;
407
+ }
408
+
409
+ // 注意这里默认是use
410
+ let method = typeof options.method === 'string' ? options.method.trim().toLowerCase() : 'use';
411
+
412
+ let guards = options.guards || [];
413
+ let isApi = options.isApi;
414
+ let name = options.name || `${options.path}`;
415
+ let description = options.description || `${options.path}`;
416
+ let handler = options.handler;
417
+ let needLogin = options.needLogin ? true : false;
418
+ let permissions = options.permissions || [];
419
+ let requestSchema = options.requestSchema || {};
420
+ let responseSchema = options.responseSchema || {};
421
+
422
+ // TODO生成接口文档说明
423
+ if (this.config.showApiDocs && isApi) {
424
+ let moduleName = options.pathPrefix || '/'; // 模块名
425
+ // 转换路径参数
426
+ const pathParams = requestSchema.params ? j2s(joi.object(requestSchema.params)).swagger : {};
427
+ // 转换查询参数
428
+ const queryParams = requestSchema.query ? j2s(joi.object(requestSchema.query)).swagger : {};
429
+ // 转换body参数
430
+ const bodyParams = requestSchema.body ? j2s(joi.object(requestSchema.body)).swagger : {};
431
+ // 生成swagger配置
432
+ const parameters = [
433
+ ...Object.entries(pathParams.properties || {}).map(([name, schema]) => ({
434
+ name,
435
+ in: 'path',
436
+ schema
437
+ })),
438
+ ...Object.entries(queryParams.properties || {}).map(([name, schema]) => ({
439
+ name,
440
+ in: 'query',
441
+ schema
442
+ }))
443
+ ];
444
+
445
+ const pathItem = {
446
+ [method]: {
447
+ tags: [moduleName],
448
+ summary: name,
449
+ description: description,
450
+ parameters: parameters,
451
+ requestBody: {
452
+ content: {
453
+ 'application/json': {
454
+ schema: bodyParams
455
+ }
456
+ }
457
+ },
458
+ responses: {
459
+ 200: {
460
+ description: 'Success response'
461
+ }
462
+ }
463
+ }
464
+ };
465
+ this.swaggerSpec.paths[options.path] = {
466
+ ...this.swaggerSpec.paths[options.path],
467
+ ...pathItem
468
+ };
469
+ }
470
+ const opt = {
471
+ path: options.path,
472
+ pathPrefix: options.pathPrefix,
473
+ method,
474
+ isApi,
475
+ name,
476
+ description,
477
+ needLogin,
478
+ permissions
479
+ }
480
+
481
+ // 添加路由,按顺序调用中间件
482
+ let router = express.Router();
483
+ let parList = [];
484
+ if (options.path) {
485
+ parList.push(options.path)
486
+ }
487
+ if (guards && guards.length) {
488
+ parList.push(...guards)
489
+ }
490
+ if (requestSchema && (requestSchema.body || requestSchema.query || requestSchema.params)) {
491
+ parList.push(payloadValidate(requestSchema));
492
+ }
493
+ parList.push(handler);
494
+ router[method](...parList);
495
+ console.log('===========>>>>>------', method)
496
+ console.log('===========>>>>>------', parList)
497
+ this.router.use(router);
498
+
499
+
500
+ apiRouteList.push(opt);
501
+ if (router.stack && router.stack[0] && router.stack[0].route) {
502
+ const rut = router.stack[0].route;
503
+ this.router.customOptions = this.router.customOptions || [];
504
+
505
+ const mtd = Object.keys(rut.methods).map(m => m.toUpperCase()).join(',');
506
+ this.router.customOptions.push({
507
+ ...opt,
508
+ path: rut.path,
509
+ methods: mtd,
510
+ matchers: router.stack[0].matchers
511
+ })
512
+
513
+ }
514
+ }
515
+ }
516
+
517
+ let filePath = path.join(this.config.ROOT_PATH, 'dist', 'apiRouteList.json')
518
+ utils.ensureFileExistence(path.resolve(filePath))
519
+ fs.writeFileSync(path.resolve(filePath), JSON.stringify(apiRouteList, null, 4))
520
+ }
521
+
522
+ /**
523
+ * @author:
524
+ * @description: 加载api路由
525
+ * @param:
526
+ * @return:
527
+ */
528
+ async loadRouter() {
529
+ // 在路由加载前插入保护中间件
530
+ this.router.use((req, res, next) => {
531
+ if (res.headersSent) return;
532
+ next();
533
+ });
534
+ // 加载模块路由
535
+ const modulesDir = path.join(this.config.SRC_PATH, "modules");
536
+ if (!fs.existsSync(modulesDir)) {
537
+ console.log('============加载模块路由完成,未发现模块路由:', modulesDir);
538
+ return
539
+ }
540
+ const dirList = this.scanDirModules(modulesDir);
541
+ // 加载普通模块(api、web等等)
542
+ await this.loadModules(dirList);
543
+
544
+ if (this.config.showApiDocs) {
545
+ // swagger生成
546
+ let apiDocsPath = this.config.apiDocsPath ? this.config.apiDocsPath : '/api-docs';
547
+ this.app.use(apiDocsPath, swaggerUi.serve, swaggerUi.setup(this.swaggerSpec));
548
+ console.log('============生成swagger接口文档');
549
+ console.log(`查看接口文档请访问网站根目录${apiDocsPath}`);
550
+ }
551
+
552
+ // 加载公共路由(404处理、错误处理)
553
+ await this.loadModules(dirList, true);
554
+ // 获取路由模块列表并输出
555
+ if (this.router.customOptions) {
556
+ let filePath = path.join(this.config.ROOT_PATH, 'dist', 'customOptions.json')
557
+ utils.ensureFileExistence(path.resolve(filePath))
558
+ fs.writeFileSync(path.resolve(filePath), JSON.stringify(this.router.customOptions, null, 4))
559
+ }
560
+
561
+ // 注册路由
562
+ this.app.use(this.router);
563
+ }
564
+ /**
565
+ * @description: 加载ssl证书
566
+ * @return {*}
567
+ */
568
+ loadSsl() {
569
+ const sslDir = path.join(this.config.SRC_PATH, "ssl");
570
+ if (!fs.existsSync(sslDir)) {
571
+ throw new Error('SSL 目录不存在');
572
+ }
573
+ const keyFiles = this.scanDirModules(sslDir, '.key');
574
+ const certFiles = this.scanDirModules(sslDir, '.crt');
575
+
576
+ // 增加证书匹配验证
577
+ const certPairs = keyFiles.reduce((acc, keyFile) => {
578
+ const certFile = certFiles.find(f =>
579
+ path.basename(f, '.crt') === path.basename(keyFile, '.key')
580
+ );
581
+ if (certFile) acc.push({ key: keyFile, cert: certFile });
582
+ return acc;
583
+ }, []);
584
+
585
+ if (!certPairs.length) {
586
+ throw new Error('未找到有效的 SSL 证书文件');
587
+ }
588
+
589
+ try {
590
+ // https配置项
591
+ return certPairs.map(pair => ({
592
+ key: fs.readFileSync(pair.key, 'UTF8'),
593
+ cert: fs.readFileSync(pair.cert, 'UTF8')
594
+ }));
595
+ } catch (error) {
596
+ throw new Error(`SSL 证书读取失败: ${error.message}`);
597
+ }
598
+ }
599
+
600
+ async modelsHook(models) {
601
+ // 独立同步控制
602
+ const syncModels = async () => {
603
+ for (const model of Object.values(models)) {
604
+ try {
605
+ console.log(`✅ Model ${model.name} 开始同步...`)
606
+ await model.sync(model.syncOptions || {
607
+ alter: process.env.ALTER_SYNC === 'true',
608
+ force: process.env.FORCE_SYNC === 'true'
609
+ });
610
+ console.log(`✅ Model ${model.name} synced successfully`);
611
+
612
+ // 插入初始化数据
613
+ if (model.initData) {
614
+ // 在syncModels循环内增加:
615
+ const t = await model.sequelize.transaction();
616
+ try {
617
+ const count = await model.count({ transaction: t });
618
+
619
+ if (count === 0) {
620
+ await model.bulkCreate(model.initData(), {
621
+ transaction: t,
622
+ validate: true
623
+ });
624
+ }
625
+ await t.commit();
626
+ } catch (error) {
627
+ await t.rollback();
628
+ throw error;
629
+ }
630
+ console.log(`✅ Initial data inserted for ${model.name}`);
631
+ }
632
+ } catch (error) {
633
+ console.error(`❌ Error syncing model ${model.name}:`, error);
634
+ }
635
+ }
636
+ };
637
+
638
+ // 根据配置同步表模型到数据库和初始化数据
639
+ await syncModels()
640
+
641
+ // 创建关联关系(不做物理外键关联,仅做逻辑关联)
642
+ Object.values(models).forEach(model => {
643
+ if (model.associate) model.associate(models);
644
+ });
645
+ }
646
+ async loadModels() {
647
+ const modelsDir = path.join(this.config.SRC_PATH, "models");
648
+ if (fs.existsSync(modelsDir)) {
649
+ // 获取所有模型文件
650
+ const modelFiles = this.scanDirModules(modelsDir, '.js');
651
+ // 按最后一个文件夹分类
652
+ const dbModels = {};
653
+ modelFiles.forEach(filePath => {
654
+ // 提取数据库
655
+ const relativePath = path.relative(modelsDir, path.dirname(filePath));
656
+ const dbDir = relativePath.split(path.sep).join('_');
657
+
658
+ if (!dbModels[dbDir]) {
659
+ dbModels[dbDir] = {};
660
+ }
661
+ const model = require(filePath);
662
+ dbModels[dbDir][model.name] = model;
663
+ })
664
+
665
+ // 注册模型
666
+ for (const dbDir in dbModels) {
667
+ if (dbModels.hasOwnProperty(dbDir)) {
668
+ const modelList = dbModels[dbDir];
669
+ await this.modelsHook(modelList)
670
+ }
671
+ }
672
+ }
673
+ }
674
+
675
+ async run(success, error) {
676
+ try {
677
+
678
+ this.beforeStart && await this.beforeStart();
679
+
680
+ // 加载本地配置
681
+ await this.loadLocalConfig();
682
+
683
+ // 加载核心(日志、favicon 图标、cookie、json、url、模板引擎、静态资源)
684
+ this.loadCore();
685
+
686
+ // 加载扩展,sequelize、mysql、redis、mailer等实例化,直接挂载到Vt下面
687
+ await this.loadExtends();
688
+
689
+ // 加载模型
690
+ await this.loadModels();
691
+
692
+ // 加载前置全局中间件
693
+ await this.loadGlobalMiddlewares();
694
+ // 加载守卫(局部)中间件
695
+ await this.loadGuardMiddlewares();
696
+
697
+ this.beforeLoadRouter && await this.beforeLoadRouter(this);
698
+
699
+ // TODO 后续要增加:加载插件路由
700
+ // 加载路由
701
+ await this.loadRouter();
702
+
703
+ const config = this.config;
704
+ const port = config.port || '81';
705
+ const useHttps = config.useHttps;
706
+
707
+ try {
708
+ if (useHttps) {
709
+ let httpsOptions = this.loadSsl();
710
+ // 创建https服务器,注意:一般设置https监听端口号443, http端口号为80。这里使用配置的端口号
711
+ let httpsServer = https.createServer(httpsOptions, this.app);
712
+
713
+ httpsServer.listen(port, function () {
714
+ console.log(`VBoot's HTTPS server is running on port ${port}`);
715
+ console.log('服务运行地址:%o', `https://localhost:${port}`);
716
+ success && success()
717
+ });
718
+ } else {
719
+ this.app.listen(port, function () {
720
+ console.log(`VBoot's HTTP server is running on port ${port}`);
721
+ console.log('服务运行地址:%o', `http://localhost:${port}`);
722
+ success && success()
723
+ });
724
+ }
725
+
726
+ } catch (e) {
727
+ error && error(e)
728
+ }
729
+ } catch (e) {
730
+ console.error('VBoot startup failed:', e);
731
+ error && error(e);
732
+ // 可以考虑进程退出
733
+ process.exit(1);
734
+ }
735
+ }
736
+ }
737
+
738
+ // 导出
739
+ module.exports = VBoot;
@@ -0,0 +1,36 @@
1
+ const path = require('path');
2
+
3
+ /**
4
+ * @description 根目录
5
+ */
6
+ const ROOT_PATH = process.cwd();
7
+
8
+ /**
9
+ * @description 程序目录
10
+ */
11
+ const SRC_PATH = path.join(ROOT_PATH, 'src');
12
+
13
+ let config = {
14
+ JSON_LIMIT: "100kb",
15
+ env: 'dev',
16
+ template: 'default',
17
+ maxAge: '1d',
18
+ corsOptions: {},
19
+ showApiDocs: true,
20
+ apiDocsPath: '/api-docs',
21
+ database: {
22
+ client: "mysql2",
23
+ host: "localhost",
24
+ port: "3306",
25
+ user: "root",
26
+ password: "123456",
27
+ database: "vtsystem",
28
+ charset: "utf8mb4",
29
+ }
30
+ }
31
+
32
+ module.exports = {
33
+ ROOT_PATH,
34
+ SRC_PATH,
35
+ ...config
36
+ }
@@ -0,0 +1,51 @@
1
+ /*
2
+ * @Author: tymon 573562310@qq.com
3
+ * @Date: 2024-08-11 19:53:34
4
+ * @LastEditors: tymon 573562310@qq.com
5
+ * @LastEditTime: 2024-08-11 19:57:17
6
+ * @FilePath: \vboot\core\lib\core.js
7
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
8
+ */
9
+ const express = require("express");
10
+ const favicon = require("serve-favicon");
11
+ const path = require("path");
12
+ const cors = require('cors'); //处理跨域
13
+ const fs = require('fs');
14
+
15
+ module.exports = async function (app, config) {
16
+
17
+ const { SRC_PATH, maxAge, JSON_LIMIT, corsOptions } = config;
18
+
19
+ // favicon 图标
20
+ let faviconPath = path.join(SRC_PATH, "public/favicon.ico");
21
+ if (fs.existsSync(faviconPath)) {
22
+ app.use(favicon(faviconPath));
23
+ } else {
24
+ console.log('未配置favicon,可在src/public目录下进行配置')
25
+ }
26
+
27
+ // 注册cors跨域处理的中间件
28
+ app.use(cors(corsOptions));
29
+ // 配置解析 application/json 格式的中间件
30
+ app.use(express.json({ limit: JSON_LIMIT }));
31
+ // 配置解析 application/x-www-form-urlencoded 格式的表单数据的中间件
32
+ app.use(express.urlencoded({ extended: false }));
33
+
34
+ // 开放静态资源文件
35
+ const publicDir = path.join(SRC_PATH, "public");
36
+ app.use(
37
+ "/public",
38
+ express.static(publicDir, {
39
+ maxAge: maxAge,
40
+ })
41
+ );
42
+
43
+ // 模板的路径
44
+ const viewsDir = path.join(SRC_PATH, 'views');
45
+ app.set('views', viewsDir);
46
+ // 模板后缀
47
+ app.set('view engine', 'html');
48
+ // 指定渲染后缀为 html 的模板引擎
49
+ app.engine('html', require('express-art-template'));
50
+
51
+ };
File without changes
@@ -0,0 +1,43 @@
1
+ /*
2
+ * 描述: 封装的joi校验中间件
3
+ */
4
+
5
+ const Joi = require('joi')
6
+
7
+ module.exports = function (schemas, options = { strict: false }) {
8
+ // 自定义校验选项
9
+ // strict 自定义属性,是否开启严格模式,默认不开启
10
+ if (!options.strict) {
11
+ // allowUnknown 允许提交未定义的参数项
12
+ // stripUnknown 过滤掉那些未定义的参数项
13
+ options = { allowUnknown: true, stripUnknown: false, ...options }
14
+ }
15
+
16
+ // 从 options 配置对象中,删除自定义的 strict 属性
17
+ delete options.strict
18
+
19
+ // TODO: 用户指定了什么 schema,就应该校验什么样的数据
20
+ return function (req, res, next) {
21
+ ['body', 'query', 'params'].forEach(key => {
22
+ // 如果当前循环的这一项 schema 没有提供,则不执行对应的校验
23
+ if (!schemas[key]) return
24
+
25
+ // 执行校验
26
+ const schema = Joi.object(schemas[key])
27
+ const { error, value } = schema.validate(req[key], options)
28
+
29
+ if (error) {
30
+ console.error('joi-error:', error);
31
+ // 校验失败
32
+ throw error
33
+ } else {
34
+ // 校验成功,把校验的结果重新赋值到 req 对应的 key 上
35
+ req[key] = value
36
+ }
37
+ })
38
+
39
+ // 校验通过
40
+ next()
41
+ }
42
+ }
43
+
@@ -0,0 +1,24 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+
4
+ // 判断文件、文件夹是否存在,不存在则创建
5
+ exports.ensureFileExistence = function (filePath) {
6
+ // 获取文件目录路径
7
+ const dirPath = path.dirname(filePath);
8
+
9
+ // 确保目录存在
10
+ if (!fs.existsSync(dirPath)) {
11
+ fs.mkdirSync(dirPath, { recursive: true });
12
+ console.log(`Directory ${dirPath} created.`);
13
+ } else {
14
+ console.log(`Directory ${dirPath} already exists.`);
15
+ }
16
+
17
+ // 确保文件存在
18
+ if (!fs.existsSync(filePath)) {
19
+ fs.writeFileSync(filePath, '', 'utf8'); // 创建空文件
20
+ console.log(`File ${filePath} created.`);
21
+ } else {
22
+ console.log(`File ${filePath} already exists.`);
23
+ }
24
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "vboot",
3
+ "version": "0.0.1",
4
+ "description": "vtcore一个基于express的轻量级框架",
5
+ "main": "core/index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "engines": {
10
+ "node": ">= 18"
11
+ },
12
+ "keywords": [
13
+ "vboot",
14
+ "vithing",
15
+ "express"
16
+ ],
17
+ "author": "tymoony",
18
+ "license": "Apache-2.0",
19
+ "dependencies": {
20
+ "art-template": "^4.13.2",
21
+ "cors": "^2.8.5",
22
+ "deepmerge": "^4.3.1",
23
+ "express": "^5.1.0",
24
+ "express-art-template": "^1.0.1",
25
+ "joi": "^17.13.3",
26
+ "joi-to-swagger": "^6.1.1",
27
+ "mysql2": "^3.11.0",
28
+ "path-to-regexp": "^8.0.0",
29
+ "sequelize": "^6.37.3",
30
+ "serve-favicon": "^2.5.0",
31
+ "swagger-jsdoc": "^6.2.8",
32
+ "swagger-ui-express": "^4.6.2"
33
+ },
34
+ "volta": {
35
+ "node": "18.20.4"
36
+ }
37
+ }
package/readme.md ADDED
@@ -0,0 +1,2 @@
1
+ 一个基于express的轻量级开发框架
2
+ 约定大于配置
package/test/index.js ADDED
@@ -0,0 +1,16 @@
1
+ const VBoot = require('../core/index');
2
+ const vt = VBoot.create({
3
+ config: {
4
+ port: 3000,
5
+ useHttps: false
6
+ },
7
+ beforeStart: () => { },
8
+ beforeLoadRouter: async () => {
9
+ console.log('=======app:');
10
+ },
11
+ });
12
+
13
+ // TODO请求数据库获取配置
14
+ vt.run(() => {
15
+ console.log("系统启动成功");
16
+ });