鸿蒙5.0实战案例:基于自定义注解和代码生成实现路由框架

往期推文全新看点(文中附带全新鸿蒙5.0全栈学习笔录)

✏️ 鸿蒙(HarmonyOS)北向开发知识点记录~

✏️ 鸿蒙(OpenHarmony)南向开发保姆级知识点汇总~

✏️ 鸿蒙应用开发与鸿蒙系统开发哪个更有前景?

✏️ 嵌入式开发适不适合做鸿蒙南向开发?看完这篇你就了解了~

✏️ 对于大前端开发来说,转鸿蒙开发究竟是福还是祸?

✏️ 鸿蒙岗位需求突增!移动端、PC端、IoT到底该怎么选?

✏️ 记录一场鸿蒙开发岗位面试经历~

✏️ 持续更新中……


场景描述

在应用开发中无论是出于工程组织效率还是开发体验的考虑,开发者都需要对项目进行模块间解耦,此时需要构建一套用于模块间组件跳转、数据通信的路由框架。

业界常见的实现方式是在编译期生成路由表。

1. 实现原理及流程

  • 在编译期通过扫描并解析ets文件中的自定义注解来生成路由表和组件注册类
  • Har中的rawfile文件在Hap编译时会打包在Hap中,通过这一机制来实现路由表的合并
  • 自定义组件通过wrapBuilder封装来实现动态获取
  • 通过NavDestination的Builder机制来获取wrapBuilder封装后的自定义组件

2. 使用ArkTS自定义装饰器来代替注解的定义

由于TS语言特性,当前只能使用自定义装饰器

使用@AppRouter装饰器来定义路由信息

// 定义空的装饰器
export function AppRouter(param:AppRouterParam) {
  return Object;
}

export interface AppRouterParam{
  uri:string;
}

自定义组件增加路由定义

@AppRouter({ uri: "app://login" })
@Component
export struct LoginView {
  build(){
    //...
  }
}

3. 实现动态路由模块

定义路由表(该文件为自动生成的路由表)

{
  "routerMap": [
    {
      "name": "app://login",   /* uri定义  */
      "pageModule": "loginModule",  /* 模块名  */
      "pageSourceFile": "src/main/ets/generated/RouterBuilder.ets",  /* Builder文件  */
      "registerFunction": "LoginViewRegister"  /* 组件注册函数  */
    }
  ]
}

应用启动时,在EntryAbility.onCreate中加载路由表

onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  DynamicRouter.init({
    libPrefix: "@app", mapPath: "routerMap"
  }, this.context);
}

export class DynamicRouter {
  // 路由初始化配置
  static config: RouterConfig;
  // 路由表
  static routerMap: Map<string, RouterInfo> = new Map();
  // 管理需要动态导入的模块,key是模块名,value是WrappedBuilder对象,动态调用创建页面的接口
  static builderMap: Map<string, WrappedBuilder<Object[]>> = new Map();
  // 路由栈
  static navPathStack: NavPathStack = new NavPathStack();
  // 通过数组实现自定义栈的管理
  static routerStack: Array<RouterInfo> = new Array();
  static referrer: string[] = [];

  public static init(config: RouterConfig, context: Context) {
    DynamicRouter.config = config;
    DynamicRouter.routerStack.push(HOME_PAGE)
    RouterLoader.load(config.mapPath, DynamicRouter.routerMap, context)
  }
  //...
}

路由表存放在src/main/resources/rawfile目录中,通过ResourceManager进行读取

export namespace RouterLoader {

  export function load(dir: string, routerMap: Map<string, RouterInfo>, context: Context) {
    const rm: resourceManager.ResourceManager = context.resourceManager;
    try {
      rm.getRawFileList(dir)
        .then((value: Array<string>) => {
          let decoder: util.TextDecoder = util.TextDecoder.create('utf-8', {
            fatal: false, ignoreBOM: true
          })
          value.forEach(fileName => {
            let fileBytes: Uint8Array = rm.getRawFileContentSync(`${dir}/${fileName}`)
            let retStr = decoder.decodeWithStream(fileBytes)
            let routerMapModel: RouterMapModel = JSON.parse(retStr) as RouterMapModel
            loadRouterMap(routerMapModel, routerMap)
          })
        })
        .catch((error: BusinessError) => {
          //...
        });
    } catch (error) {
      //...
    }
  }
}

根据URI跳转页面时,通过动态import并执行路由表中定义的registerFunction方法来实现动态注册组件

Button("跳转")
  .onClick(()=>{
    DynamicRouter.pushUri("app://settings")
  })

export class DynamicRouter {
  //...
  public static pushUri(uri: string, param?: Object, onPop?: (data: PopInfo) => void): void {
    if (!DynamicRouter.routerMap.has(uri)) {
      return;
    }
    let routerInfo: RouterInfo = DynamicRouter.routerMap.get(uri)!;
    if (!DynamicRouter.builderMap.has(uri)) {
      // 动态加载模块
      import(`${DynamicRouter.config.libPrefix}/${routerInfo.pageModule}`)
        .then((module: ESObject) => {
          module[routerInfo.registerFunction!](routerInfo)   // 进行组件注册,实际执行了下文中的LoginViewRegister方法
          DynamicRouter.navPathStack.pushDestination({ name: uri, onPop: onPop, param: param });
          DynamicRouter.pushRouterStack(routerInfo);
        })
        .catch((error: BusinessError) => {
          console.error(`promise import module failed, error code: ${error.code}, message: ${error.message}.`);
        });
    } else {
      DynamicRouter.navPathStack.pushDestination({ name: uri, onPop: onPop, param: param });
      DynamicRouter.pushRouterStack(routerInfo);
    }
  }
}

组件注册实际执行的方法为LoginViewRegister(该文件为自动生成的模版代码)

// auto-generated RouterBuilder.ets
import { DynamicRouter, RouterInfo } from '@app/dynamicRouter/Index'
import { LoginView } from '../components/LoginView'

@Builder
function LoginViewBuilder() {
  LoginView()
}

export function LoginViewRegister(routerInfo: RouterInfo) {
  DynamicRouter.registerRouterPage(routerInfo, wrapBuilder(LoginViewBuilder))
}

通过wrapBuilder将自定义组件保存在组件表

export class DynamicRouter {
  //...
  // 通过URI注册builder
  public static registerRouterPage(routerInfo: RouterInfo, wrapBuilder: WrappedBuilder<Object[]>): void {
     const builderName: string = routerInfo.name;
     if (!DynamicRouter.builderMap.has(builderName)) {
       DynamicRouter.registerBuilder(builderName, wrapBuilder);
     }
   }

   private static registerBuilder(builderName: string, builder: WrappedBuilder<Object[]>): void {
     DynamicRouter.builderMap.set(builderName, builder);
   }

  // 通过URI获取builder
  public static getBuilder(builderName: string): WrappedBuilder<Object[]> {
    const builder = DynamicRouter.builderMap.get(builderName);
    return builder as WrappedBuilder<Object[]>;
  }
}

首页Navigation通过组件表获取自定义组件Builder

@Entry
@Component
struct Index {
  build() {
    Navigation(DynamicRouter.getNavPathStack()) {
      //...
    }
    .navDestination(this.PageMap)
    .hideTitleBar(true)
  }

  @Builder
  PageMap(name: string, param?: ESObject) {
    NavDestination() {
      DynamicRouter.getBuilder(name).builder(param);
    }
  }

}

4. 实现路由表生成插件

新建插件目录etsPlugin,建议创建在HarmonyOS工程目录之外

mkdir etsPlugin
cd etsPlugin

创建npm项目

npm init

安装依赖

npm i --save-dev @types/node @ohos/hvigor @ohos/hvigor-ohos-plugin
npm i typescript handlebars

初始化typescript配置

./node_modules/.bin/tsc --init

修改tsconfig.json

{
  "compilerOptions": {
    "target": "es2021",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
    "module": "commonjs",                                /* Specify what module code is generated. */
    "strict": true,                                      /* Enable all strict type-checking options. */
    "esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
    "forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */
    "skipLibCheck": true,                                 /* Skip type checking all .d.ts files. */
    "sourceMap": true,
    "outDir": "./lib",
  },
  "include": [".eslintrc.js", "src/**/*"],
  "exclude": ["node_modules", "lib/**/*"],
}

创建插件文件src/index.ts

export function etsGeneratorPlugin(pluginConfig: PluginConfig): HvigorPlugin {
  return {
    pluginId: PLUGIN_ID,
    apply(node: HvigorNode) {
      pluginConfig.moduleName = node.getNodeName();
      pluginConfig.modulePath = node.getNodePath();
      pluginExec(pluginConfig);
    },
  };
}

修改package.json

{
  //...
  "main": "lib/index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "tsc && node lib/index.js",
    "build": "tsc"
  },
  //...
}

插件实现流程

  • 通过扫描自定义组件的ets文件,解析语法树,拿到注解里定义的路由信息
  • 生成路由表、组件注册类,同时更新Index.ets

定义插件配置

const config: PluginConfig = {
  builderFileName: "RouterBuilder.ets",  // 生成的组件注册类文件名
  builderDir: "src/main/ets/generated",  // 代码生成路径
  routerMapDir: "src/main/resources/rawfile/routerMap",  // 路由表生成路径
  scanDir: "src/main/ets/components",  // 自定义组件扫描路径
  annotation: "AppRouter",  // 路由注解
  viewKeyword: "struct",  // 自定义组件关键字
  builderTpl: "viewBuilder.tpl",  // 组件注册类模版文件
};

插件核心代码:

function pluginExec(config: PluginConfig) {
  // 读取指定自定义组件目录下的文件
  const scanPath = `${config.modulePath}/${config.scanDir}`;
  const files: string[] = readdirSync(scanPath);
  files.forEach((fileName) => {
    // 对每个文件进行解析
    const sourcePath = `${scanPath}/${fileName}`;
    const importPath = path
      .relative(`${config.modulePath}/${config.builderDir}`, sourcePath)
      .replaceAll("\\", "/")
      .replaceAll(".ets", "");

    // 执行语法树解析器
    const analyzer = new EtsAnalyzer(config, sourcePath);
    analyzer.start();

    // 保存解析结果
    console.log(JSON.stringify(analyzer.analyzeResult));
    console.log(importPath);
    templateModel.viewList.push({
      viewName: analyzer.analyzeResult.viewName,
      importPath: importPath,
    });
    routerMap.routerMap.push({
      name: analyzer.analyzeResult.uri,
      pageModule: config.moduleName,
      pageSourceFile: `${config.builderDir}/${config.builderFileName}`,
      registerFunction: `${analyzer.analyzeResult.viewName}Register`,
    });
  });

  // 生成组件注册类
  generateBuilder(templateModel, config);
  // 生成路由表
  generateRouterMap(routerMap, config);
  // 更新Index文件
  generateIndex(config);
}

语法树解析流程

  • 遍历语法树节点,找到自定义注解@AppRouter
  • 读取URI的值
  • 通过识别struct关键字来读取自定义组件类名
  • 其他节点可以忽略

核心代码:

export class EtsAnalyzer {
  sourcePath: string;
  pluginConfig: PluginConfig;
  analyzeResult: AnalyzeResult = new AnalyzeResult();
  keywordPos: number = 0;

  constructor(pluginConfig: PluginConfig, sourcePath: string) {
    this.pluginConfig = pluginConfig;
    this.sourcePath = sourcePath;
  }

  start() {
    const sourceCode = readFileSync(this.sourcePath, "utf-8");
    // 创建ts语法解析器
    const sourceFile = ts.createSourceFile(
      this.sourcePath,
      sourceCode,
      ts.ScriptTarget.ES2021,
      false
    );
    // 遍历语法节点
    ts.forEachChild(sourceFile, (node: ts.Node) => {
      this.resolveNode(node);
    });
  }

  // 根据节点类型进行解析
  resolveNode(node: ts.Node): NodeInfo | undefined {
    switch (node.kind) {
      case ts.SyntaxKind.ImportDeclaration: {
        this.resolveImportDeclaration(node);
        break;
      }
      case ts.SyntaxKind.MissingDeclaration: {
        this.resolveMissDeclaration(node);
        break;
      }
      case ts.SyntaxKind.Decorator: {
        this.resolveDecorator(node);
        break;
      }
      case ts.SyntaxKind.CallExpression: {
        this.resolveCallExpression(node);
        break;
      }
      case ts.SyntaxKind.ExpressionStatement: {
        this.resolveExpression(node);
        break;
      }
      case ts.SyntaxKind.Identifier: {
        return this.resolveIdentifier(node);
      }
      case ts.SyntaxKind.StringLiteral: {
        return this.resolveStringLiteral(node);
      }
      case ts.SyntaxKind.PropertyAssignment: {
        return this.resolvePropertyAssignment(node);
      }
    }
  }

  resolveImportDeclaration(node: ts.Node) {
    let ImportDeclaration = node as ts.ImportDeclaration;
  }

  resolveMissDeclaration(node: ts.Node) {
    node.forEachChild((cnode) => {
      this.resolveNode(cnode);
    });
  }

  resolveDecorator(node: ts.Node) {
    let decorator = node as ts.Decorator;
    this.resolveNode(decorator.expression);
  }

  resolveIdentifier(node: ts.Node): NodeInfo {
    let identifier = node as ts.Identifier;
    let info = new NodeInfo();
    info.value = identifier.escapedText.toString();
    return info;
  }

  resolveCallExpression(node: ts.Node) {
    let args = node as ts.CallExpression;
    let identifier = this.resolveNode(args.expression);
    this.parseRouterConfig(args.arguments, identifier);
  }

  resolveExpression(node: ts.Node) {
    let args = node as ts.ExpressionStatement;
    let identifier = this.resolveNode(args.expression);
    if (identifier?.value === this.pluginConfig.viewKeyword) {
      this.keywordPos = args.end;
    }
    if (this.keywordPos === args.pos) {
      this.analyzeResult.viewName = identifier?.value;
    }
  }

  resolveStringLiteral(node: ts.Node): NodeInfo {
    let stringLiteral = node as ts.StringLiteral;
    let info = new NodeInfo();
    info.value = stringLiteral.text;
    return info;
  }

  resolvePropertyAssignment(node: ts.Node): NodeInfo {
    let propertyAssignment = node as ts.PropertyAssignment;
    let propertyName = this.resolveNode(propertyAssignment.name)?.value;
    let propertyValue = this.resolveNode(propertyAssignment.initializer)?.value;
    let info = new NodeInfo();
    info.value = { key: propertyName, value: propertyValue };
    return info;
  }

}

使用模版引擎生成组件注册类

使用Handlebars生成组件注册类

const template = Handlebars.compile(tpl);
const output = template({ viewList: templateModel.viewList });

模版文件viewBuilder.tpl示例:

// auto-generated RouterBuilder.ets
import { DynamicRouter, RouterInfo } from '@app/dynamicRouter/Index'
{{#each viewList}}
import { {{viewName}} } from '{{importPath}}'
{{/each}}

{{#each viewList}}
@Builder
function {{viewName}}Builder() {
  {{viewName}}()
}

export function {{viewName}}Register(routerInfo: RouterInfo) {
  DynamicRouter.registerRouterPage(routerInfo, wrapBuilder({{viewName}}Builder))
}

{{/each}}

生成的RouterBuilder.ets代码示例:

// auto-generated RouterBuilder.ets
import { DynamicRouter, RouterInfo } from '@app/dynamicRouter/Index'
import { LoginView } from '../components/LoginView'

@Builder
function LoginViewBuilder() {
  LoginView()
}

export function LoginViewRegister(routerInfo: RouterInfo) {
  DynamicRouter.registerRouterPage(routerInfo, wrapBuilder(LoginViewBuilder))
}

将路由表和组件注册类写入文件

  • 路由表保存在rawfile目录
  • 组件注册类保存在ets代码目录
  • 更新模块导出文件Index.ets

核心代码:

function generateBuilder(templateModel: TemplateModel, config: PluginConfig) {
  console.log(JSON.stringify(templateModel));
  const builderPath = path.resolve(__dirname, `../${config.builderTpl}`);
  const tpl = readFileSync(builderPath, { encoding: "utf8" });
  const template = Handlebars.compile(tpl);
  const output = template({ viewList: templateModel.viewList });
  console.log(output);
  const routerBuilderDir = `${config.modulePath}/${config.builderDir}`;
  if (!existsSync(routerBuilderDir)) {
    mkdirSync(routerBuilderDir, { recursive: true });
  }
  writeFileSync(`${routerBuilderDir}/${config.builderFileName}`, output, {
    encoding: "utf8",
  });
}

function generateRouterMap(routerMap: RouterMap, config: PluginConfig) {
  const jsonOutput = JSON.stringify(routerMap, null, 2);
  console.log(jsonOutput);
  const routerMapDir = `${config.modulePath}/${config.routerMapDir}`;
  if (!existsSync(routerMapDir)) {
    mkdirSync(routerMapDir, { recursive: true });
  }
  writeFileSync(`${routerMapDir}/${config.moduleName}.json`, jsonOutput, {
    encoding: "utf8",
  });
}

function generateIndex(config: PluginConfig) {
  const indexPath = `${config.modulePath}/Index.ets`;
  const indexContent = readFileSync(indexPath, { encoding: "utf8" });
  const indexArr = indexContent
    .split("\n")
    .filter((value) => !value.includes(config.builderDir!));
  indexArr.push(
    `export * from './${config.builderDir}/${config.builderFileName?.replace(
      ".ets",
      ""
    )}'`
  );
  writeFileSync(indexPath, indexArr.join("\n"), {
    encoding: "utf8",
  });
}

5. 在应用中使用

修改项目的hvigor/hvigor-config.json文件,导入路由表插件

{
  "hvigorVersion": "4.2.0",
  "dependencies": {
    "@ohos/hvigor-ohos-plugin": "4.2.0",
    "@app/ets-generator" : "file:../../etsPlugin"   // 插件目录的本地相对路径,或者使用npm仓版本号
  },
  //...
}

修改loginModule模块的hvigorfile.ts文件(loginModule/hvigorfile.ts),加载插件

import { harTasks } from '@ohos/hvigor-ohos-plugin';
import {PluginConfig,etsGeneratorPlugin} from '@app/ets-generator'

const config: PluginConfig = {
    builderFileName: "RouterBuilder.ets",
    builderDir: "src/main/ets/generated",
    routerMapDir: "src/main/resources/rawfile/routerMap",
    scanDir: "src/main/ets/components",
    annotation: "AppRouter",
    viewKeyword: "struct",
    builderTpl: "viewBuilder.tpl",
}

export default {
    system: harTasks,  /* Built-in plugin of Hvigor. It cannot be modified. */
    plugins:[etsGeneratorPlugin(config)]         /* Custom plugin to extend the functionality of Hvigor. */
}

在loginModule模块的oh-package.json5中引入动态路由模块依赖

{
  "name": "loginmodule",
  "version": "1.0.0",
  "description": "Please describe the basic information.",
  "main": "Index.ets",
  "author": "",
  "license": "Apache-2.0",
  "dependencies": {
    "@app/dynamicRouter": "file:../routerModule"
  }
}

在loginModule模块的自定义组件中使用@AppRouter定义路由信息

@AppRouter({ uri: "app://login" })
@Component
export struct LoginView {
  build(){
    //...
  }
}

在entry中的oh-package.json5中引入依赖

{
  "name": "entry",
  "version": "1.0.0",
  "description": "Please describe the basic information.",
  "main": "",
  "author": "",
  "license": "",
  "dependencies": {
    "@app/loginModule": "file:../loginModule",
    "@app/commonModule": "file:../commonModule",
    "@app/dynamicRouter": "file:../routerModule"
  }
}

在entry中的build-profile.json5中配置动态import

{
  "apiType": "stageMode",
  "buildOption": {
    "arkOptions": {
      "runtimeOnly": {
        "packages": [
          "@app/loginModule",  // 仅用于使用变量动态import其他模块名场景,静态import或常量动态import无需配置。
          "@app/commonModule"  // 仅用于使用变量动态import其他模块名场景,静态import或常量动态import无需配置。
        ]
      }
    }
  },
  //...
}

在entry中的EntryAbility.onCreate中初始化路由组件

onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  DynamicRouter.init({
    libPrefix: "@app", mapPath: "routerMap"
  }, this.context);
}

组件内使用pushUri进行跳转

Button("立即登录", { buttonStyle: ButtonStyleMode.TEXTUAL })
  .onClick(() => {
    DynamicRouter.pushUri("app://login")
  })
  .id("button")

在entry模块执行Run/Debug,即可在编译时自动生成路由表配置并打包运行。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/973519.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

让浏览器AI起来:基于大模型Agent的浏览器自动化工具

最近有个非常火的项目,利用大模型Agent驱动浏览器完成各种操作,如网页搜索、爬虫分析、机票酒店预定、股票监控等,号称全面替代所有在浏览器上的操作,试用方式还是比较简单的,以下将进行简单介绍。 快速开始 通过pip安装: pip install browser-use安装web自动化框架:…

模电知识点总结(6)

1.选取频率高于1000Hz的信号时&#xff0c;可选用高通滤波器&#xff1b;抑制50Hz的交流干扰时&#xff0c;可选用带阻滤波器如果希望抑制500Hz以下的信号&#xff0c;可选用高通滤波器。 2.有用信号频率高于1000Hz&#xff0c;可选用高通滤波器&#xff1b;希望抑制50Hz的交流…

MyBatis:动态SQL高级标签使用方法指南

一、引言 目前互联网大厂在搭建后端Java服务时&#xff0c;常使用Springboot搭配Mybatis/Mybatis-plus的框架。Mybatis/Mybatis-plus之所以能成为当前国内主流的持久层框架&#xff0c;与其本身的优点有关&#xff1a;支持定制动态 SQL、存储过程及高级映射&#xff0c;简化数…

快速入门——Axios网络请求

学习自哔哩哔哩上的“刘老师教编程”&#xff0c;具体学习的网站为&#xff1a;11.Axios网络请求_哔哩哔哩_bilibili&#xff0c;以下是看课后做的笔记&#xff0c;仅供参考。 第一节Axios的使用 第二节与Vue整合 第三节跨域 第一节Axios的使用 在实际项目开发中&#xff0…

Typora的Github主题美化

对Typora的Github主题进行一些自己喜欢的修改&#xff0c;主要包括&#xff1a;字体、代码块、表格样式 美化前&#xff1a; 美化后&#xff1a; 字体更换 之前便看上了「中文网字计划」的「朱雀仿宋」字体&#xff0c;于是一直想更换字体&#xff0c;奈何自己拖延症作祟&#…

DeepSeek 助力 Vue 开发:打造丝滑的 键盘快捷键(Keyboard Shortcuts)

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享一篇文章&#xff01;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495; 目录 Deep…

[深度学习][python]yolov12+bytetrack+pyqt5实现目标追踪

【算法介绍】 实时目标检测因其低延迟特性而持续受到广泛关注&#xff0c;具有重要的实际应用价值[4, 17, 24, 28]。其中&#xff0c;YOLO系列[3, 24, 28, 29, 32, 45-47, 53, 57, 58]通过有效平衡延迟与精度&#xff0c;在该领域占据主导地位。尽管YOLO的改进多集中在损失函数…

Python大数据可视化:基于大数据技术的共享单车数据分析与辅助管理系统_flask+hadoop+spider

开发语言&#xff1a;Python框架&#xff1a;flaskPython版本&#xff1a;python3.7.7数据库&#xff1a;mysql 5.7数据库工具&#xff1a;Navicat11开发软件&#xff1a;PyCharm 系统展示 管理员登录 管理员功能界面 场地信息界面 单车信息界面 归还信息界面 共享单车界面 系…

ssm-day06 ssm整合

从springMVC总结再回顾一下 60节 整合就是应用框架&#xff0c;并且把这个框架放到IOC容器中 web容器&#xff1a;装springMVC和controller相关的web组件 root容器&#xff1a;装业务和持久层相关的组件 子容器可以引用父容器中的组件&#xff0c;父容器不能调子容器 一个容器…

MATLAB基础学习相关知识

MATLAB安装参考&#xff1a;抖音-记录美好生活 MATLAB基础知识学习参考&#xff1a;【1小时Matlab速成教程-哔哩哔哩】 https://b23.tv/CnvHtO3 第1部分&#xff1a;变量定义和基本运算 生成矩阵&#xff1a; % 生成矩阵% 直接法% ,表示行 ;表示列 a [1,2,3;4,5,6;7,8,9];%…

Windows - 通过ssh打开带有图形界面的程序 - 一种通过计划任务的曲折实现方式

Windows(奇思妙想) - 通过ssh打开带有图形界面的程序 - 一种通过计划任务的曲折实现方式 前言 Windows启用OpenSSH客户端后就可以通过SSH的方式访问Windows了。但是通过SSH启动的程序&#xff1a; 无法显示图形界面会随着SSH进程的结束而结束 于是想到了一种通过执行“计划…

WPS接入deepseek-OfficeAI助手插件下载

功能简介 OfficeAI 助手 是一款免费的智能AI办公工具软件&#xff0c;专为 Microsoft Office 和 WPS 用户打造。 无论你是在寻找如何输入“打勾&#xff08;√&#xff09;符号”的方法&#xff0c;还是想知道“怎么在插入表格前添加文字”&#xff0c;或者“该用哪个公式”&a…

【JavaEE进阶】Spring MVC(4)-图书管理系统案例

欢迎关注个人主页&#xff1a;逸狼 创造不易&#xff0c;可以点点赞吗 如有错误&#xff0c;欢迎指出~ 图书管理系统 创建书籍类BookInfo import lombok.Data;import java.math.BigDecimal;Data //这个类基本上是和数据库对应起来的 public class BookInfo {private Integer id…

路由器的WAN口和LAN口有什么区别?

今时今日&#xff0c;移动终端盛行的时代&#xff0c;WIFI可以说是家家户户都有使用到的网络接入方式。那么路由器当然也就是家家户户都不可或缺的设备了。而路由器上的两个实现网络连接的基础接口 ——WAN 口和 LAN 口&#xff0c;到底有什么区别&#xff1f;它们的功能和作用…

AI客服-接入deepseek大模型到微信(本地部署deepseek集成微信自动收发消息)

1.本地部署 1.1 ollama Ollama软件通过其高度优化的推理引擎和先进的内存管理机制&#xff0c;显著提升了大型语言模型在本地设备上的运行效率。其核心采用了量化技术&#xff08;Quantization&#xff09;以降低模型的计算复杂度和存储需求&#xff0c;同时结合张量并行计算&…

基于COSTAR模型的内容创作:如何用框架提升写作质量

目录 前言1. Context&#xff08;上下文&#xff09;&#xff1a;理解背景&#xff0c;奠定写作基础1.1 何为上下文1.2 上下文的作用1.3 案例解析 2. Objective&#xff08;目标&#xff09;&#xff1a;明确写作方向&#xff0c;避免跑题2.1 确立目标2.2 如何设定目标2.3 案例…

kafka-集群缩容

一. 简述&#xff1a; 当业务增加时&#xff0c;服务瓶颈&#xff0c;我们需要进行扩容。当业务量下降时&#xff0c;为成本考虑。自然也会涉及到缩容。假设集群有 15 台机器&#xff0c;预计缩到 10 台机器&#xff0c;那么需要做 5 次缩容操作&#xff0c;每次将一个节点下线…

DeepSeek 提示词:定义、作用、分类与设计原则

&#x1f9d1; 博主简介&#xff1a;CSDN博客专家&#xff0c;历代文学网&#xff08;PC端可以访问&#xff1a;https://literature.sinhy.com/#/?__c1000&#xff0c;移动端可微信小程序搜索“历代文学”&#xff09;总架构师&#xff0c;15年工作经验&#xff0c;精通Java编…

基于vue和微信小程序的校园自助打印系统(springboot论文源码调试讲解)

第3章 系统设计 3.1系统功能结构设计 本系统的结构分为管理员和用户、店长。本系统的功能结构图如下图3.1所示&#xff1a; 图3.1系统功能结构图 3.2数据库设计 本系统为小程序类的预约平台&#xff0c;所以对信息的安全和稳定要求非常高。为了解决本问题&#xff0c;采用前端…

大数据组件(四)快速入门实时数据湖存储系统Apache Paimon(3)

Paimon的下载及安装&#xff0c;并且了解了主键表的引擎以及changelog-producer的含义参考&#xff1a; 大数据组件(四)快速入门实时数据湖存储系统Apache Paimon(1) 利用Paimon表做lookup join&#xff0c;集成mysql cdc等参考&#xff1a; 大数据组件(四)快速入门实时数据…