看完这篇,要是还不会 MidwayJs,那你顺着网线来打我

MidwayJs Midway (midwayjs.org)、阿里巴巴、node框架、基于koa2、学习分享,官网默认示例也是基于用koa2(不知道为啥不用自家出的egg,不理解)。可能是egg对于ts的支持不是特别友好吧(纯属个人认为)。

话痨时刻

可能会有人回复,文章不错,我选nest 。这这这,咋说呢,其实刚开始了解到这两个框架时,俺也在纠结,不知道该选哪个好,也在网上找了一些关于两者的文章和对比,也很苦劳,不知道选啥好,但最终还是向着"自家人"了。这里请允许俺解释一下,俺为啥决定选 MidwayJs

  • 毕竟是 "自家人" 开发的,中文文档比较通俗易懂,比较符合国人的开发习惯,还用官方群交流答疑;
  • nest是基于express,而Midway是基于koa2的,两者的底层不同(也不能这样说,毕竟人家koa2也是基于express的),俺个人刚开始习惯用koa2去写服务;
  • next官网说它借鉴了angular的理念,那时候俺只听说过angular,并没有真正去了解过,nest比较符合国外开发者的习惯,有中文文档但都是英译的或国内个人开发者总结的;
  • 都说 技术无国界 ,是真的无国界吗!

以上都是在俺的个人理解下,才决定选用Midway,当然,不管是nest还是Midway,各有各的优势和缺点,根据个人喜好和环境去选择,它们都是很不错的框架;

第一个服务

服务技术选型:

  • node
  • pnpm
  • midway
  • typescript
  • kao2
  • typeorm
  • mysql
  • redis
  • swagger

实现基于用户相关的接口和增删改查的demo

初始化创建

cmd 复制代码
pnpm create midway
cmd 复制代码
cd midway-project
pnpm install
pnpm dev
# 启动后浏览器访问:http://127.0.0.1:7001

调整ESLint配置

为了保证代码分隔统一,我们调整下ESLint配置

cmd 复制代码
// .prettierrc.js
module.exports = {
  ...require('mwts/.prettierrc.json'),
  semi: false,
  printWidth: 100,
  tabWidth: 2,
  useTabs: false,
  singleQuote: true,
  endOfLine: 'lf',
  trailingComma: 'none',
}

ESLint 中文网 (nodejs.cn),不知道如何配置的话可以去官网查。

项目基础结构

bash 复制代码
├─logs			  # 日志文件
├─node_modules	          # 项目依赖
├─src                     # 源码目录
│  ├─config               # 配置
│  ├─controller           # 控制器
│  ├─entity               # 数据对象模型
│  ├─filter               # 过滤器
│  ├─middleware           # 中间件
│  ├─service              # 服务类
│  ├─configurations.ts    # 服务生命周期管理及配置
│  └─interface.ts         # 接口定义
├─test                    # 测试类目录
├─bootstrap.js            # 启动入口
├─package.json            # 包管理配置
├─tsconfig.json           # TypeScript 编译配置文件

使用vscode启动项目并调试

你可以不输入命令行,直接点击小绿三角形,就可以运行项目,当然,你也可以去package.json中可以看到调试字样,点击选择对应的命令即可。(俺只是觉得这个方法更帅一点,就这样😎)

json 复制代码
// 覆盖.vscode/launch.json初始配置
{
    // 使用 IntelliSense 了解相关属性。
    // 悬停以查看现有属性的描述。
    // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [{
        "name": "Midway Local",
        "type": "node",
        "request": "launch",
        "cwd": "${workspaceRoot}",
        "runtimeExecutable": "pnpm",
        "windows": {
            "runtimeExecutable": "pnpm.cmd"
        },
        "runtimeArgs": [
            "dev"
        ],
        "env": {
            "NODE_ENV": "local"
        },
        "console": "integratedTerminal",
        "restart": true,
        "autoAttachChildProcesses": true
    }]
}

数据库mysql

可能有滴人不会用 docker ,那俺就在本地创建个数据库。

cmd 复制代码
mysql -u root -p
password: ******
create database twhc;

使用TypeORM

TypeORM:(github.com)node.js现有社区最成熟的对象关系映射器(ORM )。

相关文档:

安装 typeorm 相关依赖,提供数据库ORM:

cmd 复制代码
pnpm i @midwayjs/typeorm@3 typeorm --save

src/configuration.ts 引入 orm 组件:

ts 复制代码
import { Configuration, App } from '@midwayjs/core';
import * as koa from '@midwayjs/koa';
import * as orm from '@midwayjs/typeorm';
// 略

@Configuration({
  imports: [
    koa,
    orm,
    // 略
  ],
  // 略
})
// 略

安装数据库mysql

cmd 复制代码
pnpm install mysql2 --save

配置src/config/config.default.ts文件:

ts 复制代码
import { MidwayConfig } from '@midwayjs/core';

export default {
  // use for cookie sign key, should change to your own and keep security
  keys: '1701053437454_8538',
  koa: {
    port: 7001,
  },
  typeorm: {
    dataSource: {
      default: {
        // 单个数据库
        type: 'mysql',
        host: 'localhost',
        port: 3306,
        username: 'root',
        password: '123456',
        database: 'twhc',
        entities: ['**/entity/*{.ts,.js}'], // 扫描entity文件夹
        synchronize: true, // 如果第一次使用,不存在表,有同步的需求可以写 true,注意会丢数据
        logging: true
      }
    }
  }
} as MidwayConfig;

数据库客户端连接已创建的数据库,俺用的是 DataGrip 2023.2.2 推荐使用Navicat,它对 mysql 的支持更好,但俺用DataGrip 用习惯了,两个软件都是收费的,免费的推荐 DBeaver Download | DBeaver Community --- 下载 |DBeaver 社区 这个挺好用的:

创建实体模型

entity 文件夹下创建个简单滴 user.ts ,使用 Entity 来定义一个实体模型类:(实现数据库同步更新数据表和字段)

ts 复制代码
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'

@Entity('user')
export class User {
  @PrimaryGeneratedColumn()
  id: number

  @Column()
  username: string

  @Column()
  password: string
}

启动服务,查看数据库客户端:

测试一下typeorm,改造src/controller/home.controller.ts文件:

ts 复制代码
import { Controller, Get } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { User } from '../entity/user';
import { Repository } from 'typeorm';

@Controller('/')
export class HomeController {
  // 自动注入模型
  @InjectEntityModel(User)
  userModel: Repository<User>;
  @Get('/')
  async home(): Promise<User[]> {
    // 查询user表数据
    return await this.userModel.find();
  }
}

出现以下图示,表示没问题,因为还没有插入数据,所以为空:

手动在数据库中添加一条数据,再测试一下:

缓存redis

使用docker安装redis:

小提示: 如果你不会使用 docker 的话,你可以看看俺之前写的文章:学习前端,还是要会点 docker 的,瞅瞅这篇文章,新手入门必备 - 掘金 (juejin.cn)

拉取redis镜像:

bash 复制代码
# 搜索镜像
docker search redis
# 拉取
docker pull redis
# 创建目录用于挂载
mkdir -p /home/redis/myredis /home/redis/myredis/data

myredis.conf 是俺手动上传的 (redis.conf的标准文件在redis官网也可以找到):

conf 复制代码
# bind 192.168.1.100 10.0.0.1
# bind 127.0.0.1 ::1
#bind 127.0.0.1

protected-mode no

port 6379

tcp-backlog 511

requirepass 123456

timeout 0

tcp-keepalive 300

daemonize no

supervised no

pidfile /var/run/redis_6379.pid

loglevel notice

logfile ""

databases 30

always-show-logo yes

save 900 1
save 300 10
save 60 10000

stop-writes-on-bgsave-error yes

rdbcompression yes

rdbchecksum yes

dbfilename dump.rdb

dir ./

replica-serve-stale-data yes

replica-read-only yes

repl-diskless-sync no

repl-disable-tcp-nodelay no

replica-priority 100

lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
replica-lazy-flush no

appendonly yes

appendfilename "appendonly.aof"

no-appendfsync-on-rewrite no

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

aof-load-truncated yes

aof-use-rdb-preamble yes

lua-time-limit 5000

slowlog-max-len 128

notify-keyspace-events ""

hash-max-ziplist-entries 512
hash-max-ziplist-value 64

list-max-ziplist-size -2

list-compress-depth 0

set-max-intset-entries 512

zset-max-ziplist-entries 128
zset-max-ziplist-value 64

hll-sparse-max-bytes 3000

stream-node-max-bytes 4096
stream-node-max-entries 100

activerehashing yes

hz 10

dynamic-hz yes

aof-rewrite-incremental-fsync yes

rdb-save-incremental-fsync yes

启动redis容器:

bash 复制代码
docker run --restart=always \
	--log-opt max-size=100m \
	--log-opt max-file=2 \
	-p 6379:6379 \
	--name myredis \
	-v /home/redis/myredis/myredis.conf:/etc/redis/redis.conf \
	-v /home/redis/myredis/data:/data \
	-d redis redis-server /etc/redis/redis.conf  \
	--appendonly yes  \
	--requirepass 123456
  1. --restart=always 总是开机启动
  2. --log 是日志方面的
  3. -p 6379:6379 将6379端口挂载出去(加粗是容器端口,:前面是宿主机端口)
  4. --name 给这个容器取一个名字
  5. -v 数据卷挂载: /home/redis/myredis/myredis.conf:/etc/redis/redis.conf 这里是将 liunx路径下的myredis.conf 和redis下的redis.conf 挂载在一起。 /home/redis/myredis/data:/data 这个同上
  6. -d redis 表示后台启动redis
  7. redis-server /etc/redis/redis.conf 以配置文件启动redis,加载容器内的conf文件,最终找到的是挂载的目录 /etc/redis/redis.conf 也就是liunx下的/home/redis/myredis/myredis.conf
  8. --appendonly yes 开启redis 持久化
  9. --requirepass 123456 设置密码 (设置密码只有好处,信俺没错😁)

查看容器运行日志:(--since 30m 是查看此容器30分钟之内的日志情况,同时也表示可成功运行)

bash 复制代码
docker logs --since 30m myredis

进入容器:(此处跟着的 redis-cli 是直接将命令输在上面了)

bash 复制代码
docker exec -it myredis redis-cli

进入后,你还需输入密码:(不然会出现如下报错)

bash 复制代码
auth 123456

当出现OK字样时,就可以正常使用redis相关命令了。

redis常用命令_redis 控制台命令-CSDN博客,该篇文章有redis的一些常用命令(为啥选着用控制台去查看redis呢,啊,这这这,因为穷,那些redis客户端连接工具都要钱呀,晓得不。当下的业务场景还用不到redis那么频繁,只用到了存储就行了)

在项目中安装redis依赖:

cmd 复制代码
pnpm i @midwayjs/redis@3 --save

引入redis组件,在 src/configuration.ts 中导入:

ts 复制代码
import { Configuration, App } from '@midwayjs/core';
// 略
import * as redis from '@midwayjs/redis';
import * as validate from '@midwayjs/validate';
// 略
​
@Configuration({
  imports: [
    // 略
    redis,
    validate,
    // 略
  ],
  // 略
})
// 略

src/config/config.default.ts配置redis:

ts 复制代码
import { MidwayConfig } from '@midwayjs/core';
​
export default {
  // 略
  redis: {
    client: {
      port: 6379, // Redis port
      host: "192.168.169.128", // Redis host
      password: "123456",
      db: 0,
    }
  }
} as MidwayConfig;

使用redis服务:

ts 复制代码
import { Controller, Get, Inject } from '@midwayjs/core';
// 略
import { RedisService } from '@midwayjs/redis';
​
@Controller('/')
export class HomeController {
  // 自动注入redis服务
  @Inject()
  redisService: RedisService;
 // 略
  async home(): Promise<string> {
    await this.redisService.set('username', '吴某凡');
    return await this.redisService.get('username');
  }
}

连接的是索引为0的库:

bash 复制代码
# 切换到该索引下的库
select 0
# 表示查看该库下所有的key
keys *
# get key 查看key的值
get username

swagger接口文档

Swagger框架,用于快速生成、描述、调用和可视化 RESTful 风格的 Web 服务。它可以在线快速生成接口文档,以及快速测试接口。

安装相关依赖:

cmd 复制代码
pnpm install @midwayjs/swagger@3 --save
# 如果想要在服务器上输出 Swagger API 页面,则需要将 swagger-ui-dist 安装到依赖中
pnpm install swagger-ui-dist --save-dev

开启组件:在 configuration.ts 中(可以配置启用的环境,比如下面的代码指的是"只在 local 环境下启用")

ts 复制代码
// 略
import * as swagger from '@midwayjs/swagger';
// 略
​
@Configuration({
  imports: [
    // 略
    {
      component: swagger,
      enabledEnvironment: ['local']
    },
    // 略
  ],
  // 略
})
// 略

然后启动项目,访问地址:

路径可以通过 swaggerPath 参数配置。

多语言国际化

cmd 复制代码
pnpm i @midwayjs/i18n@3 --save

使用组件:在 configuration.ts 中:

ts 复制代码
// 略
import * as i18n from '@midwayjs/i18n';
// 略
​
@Configuration({
  imports: [
    // 略
    i18n,
    // 略
  ],
  // 略
})
// 略

配置多语言方案:新建 src/locale 目录,用于放置文案文件

bash 复制代码
# 目录结构
├── src
│   ├── locales
|   │   ├── en_US.json      # 英文
|   │   └── zh_CN.json      # 中文
json 复制代码
// en_US.json
{
  "hello": "Hello"
}
json 复制代码
// zh_CN.json
{
  "hello": "你好"
}

src/config/config.default.ts 加入这两个 JSON,其中 default 是语言的默认分组:

ts 复制代码
import { MidwayConfig } from '@midwayjs/core';
​
export default {
  // 略
  i18n: {
    localeTable: {
      en_US: {
        default: require('../locales/en_US')
      },
      zh_CN: {
        default: require('../locales/zh_CN')
      }
    }
  }
} as MidwayConfig;

home.controller.ts 使用一下:

ts 复制代码
// 略
import { MidwayI18nService } from '@midwayjs/i18n';
​
@Controller('/')
export class HomeController {
  // 自动注入i18n服务
  @Inject()
  i18nService: MidwayI18nService;
  @Get('/')
  async home(): Promise<string> {
    return await this.i18nService.translate('hello', {args: {username: '小甜甜'}, locale: 'en_US'})
  }
}

locale: 'zh_CN' 后:

统一参数校验

选着刚开始的架构创建的项目默认安装了 @midwayjs/validate 。新建 src/dto/user.ts :

ts 复制代码
import { Rule, RuleType } from '@midwayjs/validate';
​
export class UserDTO {
  @Rule(RuleType.string().required())
  password: string;
}
ts 复制代码
// src/controller/home.controller.ts
import { Controller, Body, Inject, Post } from '@midwayjs/core';
// 略
import { UserDTO } from '../dto/user';
​
@Controller('/')
export class HomeController {
  // 略
  @Post('/')
  async home(@Body() user: UserDTO): Promise<void> {
    console.log(user);
  }
}

使用swagger-ui测试一下,先传一个空对象给后端:

传入个password测试一下:

自定义报错文本:

ts 复制代码
// src/dto/user.ts
@Rule(RuleType.string().required().error(new Error('密码不能为空!')))
  password: string;

统一异常处理

在统一参数校验中,当校验失败时,Response body放回的是html,不想要这要的错误形式,可以换成json格式。

Midway提供了一个内置的异常处理器,负责处理应用程序中所有未处理的异常。当您的应用程序代码抛出一个异常处理时,该处理器就会捕获该异常,然后等待用户处理。

异常处理器的执行位置处于中间件之后,所以它能拦截所有的中间件和业务抛出的错误。

新建 filter/validate.filter.ts

ts 复制代码
import { Catch } from '@midwayjs/decorator';
import { MidwayValidationError } from '@midwayjs/validate';
import { Context } from '@midwayjs/koa';
import { MidwayI18nService } from '@midwayjs/i18n';
​
@Catch(MidwayValidationError)
export class ValidateErrorFilter {
  async catch(err: MidwayValidationError, ctx: Context) {
    // 获取国际化服务
    const i18nService = await ctx.requestContext.getAsync(MidwayI18nService);
    // 翻译
    const message = i18nService.translate(err.message) || err.message;
    // 未捕获的错误,是系统错误,错误码是500
    ctx.status = 422;
    return {
      code: 422,
      message,
    };
  }
}

configuration.ts文件中,注册刚才我们创建的过滤器:

ts 复制代码
import { Catch } from '@midwayjs/decorator';
import { MidwayValidationError } from '@midwayjs/validate';
import { Context } from '@midwayjs/koa';
import { MidwayI18nService } from '@midwayjs/i18n';
​
@Catch(MidwayValidationError)
export class ValidateErrorFilter {
  async catch(err: MidwayValidationError, ctx: Context) {
    // 获取国际化服务
    const i18nService = await ctx.requestContext.getAsync(MidwayI18nService);
    // 翻译
    const message = i18nService.translate(err.message) || err.message;
    // 未捕获的错误,是系统错误,错误码是500
    ctx.status = 422;
    return {
      code: 422,
      message,
    };
  }
}

公共业务异常处理

在开发过程中,可能会需要做一些业务校验,业务校验的时候,我们需要对外抛出异常,这时候我们需要封装公共的业务异常类,和业务异常过滤器

创建 src/common/common.error.ts

ts 复制代码
import { MidwayError } from '@midwayjs/core'
​
export class CommonError extends MidwayError {
  constructor(message: string) {
    super(message)
  }
}

在filter下新建 common.filter.ts

ts 复制代码
import { Catch } from '@midwayjs/decorator'
import { CommonError } from '../common/common.error'
import { Context } from '@midwayjs/koa'
import { MidwayI18nService } from '@midwayjs/i18n'
​
@Catch(CommonError)
export class CommonErrorFilter {
  async catch(err: CommonError, ctx: Context) {
    // 获取国际化服务
    const i18nService = await ctx.requestContext.getAsync(MidwayI18nService)
    // 翻译
    const message = i18nService.translate(err.message) || err.message
    // 未捕获的错误,是系统错误,错误码是500
    ctx.status = 400
    return {
      code: 400,
      message
    }
  }
}

src/configuration.ts中注册过滤器:

ts 复制代码
// 略
import { CommonErrorFilter } from './filter/common.filter'
​
// 略
export class MainConfiguration {
  @App('koa')
  app: koa.Application
​
  async onReady() {
    // 略
    // add filter
    this.app.useFilter([ValidateErrorFilter, CommonErrorFilter])
  }
}

因为common.filter.ts 使用了国际化:

json 复制代码
// zh_CN.json
{
  "hello": "你好 {username}",
  "error": "出错啦"
}

测试:在home.controller.ts中

ts 复制代码
// 略
import { CommonError } from '../common/common.error'
​
@Controller('/')
export class HomeController {
  // 略
  @Post('/')
  async home(): Promise<void> {
    throw new CommonError('error')
  }
}

打印日志

对于后端来说日志还是很重要的,有利于后期定位线上bug,midway也内置了一套日志组件,用起来很简单

ts 复制代码
// home.controller.ts
// 略
// 日志打印
import { ILogger } from '@midwayjs/logger'
​
@Controller('/')
export class HomeController {
  @Inject()
  logger: ILogger
​
  @Post('/')
  async home(@Body() user: UserDTO): Promise<void> {
    this.logger.info('hello')
    console.log(user)
  }
}

测试

测试还是很重要的,默认脚手架中,已经提供了这东西,所以你可以开箱即用的运行测试,仔细了解可看官方文档测试 | Midway (midwayjs.org)。俺这就不举例了,官方文档有举例。复制粘贴就行了。

demo

实现基于用户表增删改查的demo

修改用户实体类

俺将 entity/user.ts 修改为 entity/user.entity.ts ,不想与其他目录下的文件名重复(自己单纯洁癖,不改也行):

详细的参数配置可查看文档 TypeORM | Midway (midwayjs.org)

你可具体对应列的类型,这要可让数据库更加灵活,相关文档 实体 | TypeORM 中文文档 | TypeORM 中文网 (bootcss.com)

ts 复制代码
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'
​
@Entity('user')
export class User {
  @PrimaryGeneratedColumn({ comment: '主键ID' })
  id: number
​
  @Column({ comment: '用户头像', nullable: true })
  avatarUrl: string
​
  @Column({ length: 50, unique: true, comment: '用户名' })
  username: string
​
  @Column({ length: 30, comment: '密码' })
  password: string
​
  @Column({ length: 11, default: '', comment: '手机号' })
  phone: string
​
  @Column({ type: 'bigint', nullable: true, comment: '更新用户ID' })
  updaterId: number
​
  @Column({ type: 'bigint', nullable: true, comment: '创建用户ID' })
  createrId: number
​
  @Column({ type: 'int', default: 1, comment: '状态 1 正常 0 禁用' })
  status: number
​
  @CreateDateColumn({ comment: '创建时间' })
  createTime: Date
​
  @UpdateDateColumn({ comment: '更新时间' })
  updateTime: Date
}

对应的数据库结构:

用户信息参数校验

俺将 dto/user.ts 文件名改成了 user.dto.ts :使用到swagger来显示相关信息,使用其中的 ApiProperty 将其中的每个属性都进行了定义

ts 复制代码
import { Rule, RuleType } from '@midwayjs/validate'
import { ApiProperty } from '@midwayjs/swagger'
​
export class UserDTO {
  @ApiProperty({ description: '用户id' })
  @Rule(RuleType.allow(null))
  id?: number
​
  @ApiProperty({ description: '用户名' })
  @Rule(RuleType.string().required().error(new Error('用户名不能为空!')))
  username: string
​
  @ApiProperty({ description: '密码' })
  @Rule(RuleType.string().required().error(new Error('密码不能为空!')))
  password: string
}

用户服务层

创建 service/user.service.ts

你要是看不懂这上面的有些命令,那就在官网上搜,官网上啥都有。

ts 复制代码
import { Provide } from '@midwayjs/core'
import { InjectEntityModel } from '@midwayjs/typeorm'
import {  Repository } from 'typeorm'
import { User } from '../entity/user.entity'
import { IUserOptions } from '../interface'
​
@Provide()
export class UserService {
  @InjectEntityModel(User)
  userModel: Repository<User>
​
  // 新增
  async cerate(user: User) {
    await this.userModel.save(user)
    return user
  }
​
  async getUser(options: IUserOptions) {
    return {
      uid: options.uid,
      username: 'mockedName',
      phone: '12345678901',
      email: 'xxx.xxx@xxx.com'
    }
  }
}

有的代码段是脚手架自带的,俺嫌麻烦这就删除了。

用户控制层

创建 controller/user.controller.ts 文件:(提一嘴,@midwayjs/core和@midwayjs/decorator中都有类似的API,调用那个的都行,俺根据脚手架默认的来用@midwayjs/core里的)

ts 复制代码
import { Body, Controller, Inject, Post, Provide, ALL } from '@midwayjs/core'
import { Validate } from '@midwayjs/validate'
import { UserDTO } from '../dto/user.dto'
import { UserService } from '../service/user.service'
import { User } from '../entity/user.entity'
​
@Provide()
@Controller('/user')
export class UserController {
  // 注入用户服务层
  @Inject()
  userService: UserService
​
  // 创建用户
  @Post('/')
  @Validate()
  async create(@Body(ALL) data: UserDTO) {
    const user = new User()
    user.username = data.username
    user.password = data.password
    return await this.userService.create(user)
  }
}

http://127.0.0.1:7001/swagger-ui/index.html里进行测试:

在之前的用户信息参数校验中结合了swagger,使得在swagger文档中有校验参数提示:

好啦好啦

好啦好啦,不能再继续写了,再写的话就不是借鉴了,那就是 Ctrl+C+V 了。如果你还想听俺继续唠的话,可以去俺的gitee去看README.md README.md · 逗逗不秃头/midway-stud... ,只要我还在学midway就会持续更新仓库,直到把它做成一个完整的后端服务为止。但是。前端小付 这个博主,反正对于刚接触midway的俺是真喜欢呀。讲滴嘎嘎好,推荐推荐。

借鉴文章

相关推荐
学习ing小白32 分钟前
JavaWeb - 5 - 前端工程化
前端·elementui·vue
真的很上进1 小时前
【Git必看系列】—— Git巨好用的神器之git stash篇
java·前端·javascript·数据结构·git·react.js
胖虎哥er1 小时前
Html&Css 基础总结(基础好了才是最能打的)三
前端·css·html
qq_278063711 小时前
css scrollbar-width: none 隐藏默认滚动条
开发语言·前端·javascript
.ccl1 小时前
web开发 之 HTML、CSS、JavaScript、以及JavaScript的高级框架Vue(学习版2)
前端·javascript·vue.js
小徐不会写代码1 小时前
vue 实现tab菜单切换
前端·javascript·vue.js
林太白2 小时前
❤Node09-用户信息token认证
数据库·后端·mysql·node.js
2301_765347542 小时前
Vue3 Day7-全局组件、指令以及pinia
前端·javascript·vue.js
ch_s_t2 小时前
新峰商城之分类三级联动实现
前端·html
辛-夷2 小时前
VUE面试题(单页应用及其首屏加载速度慢的问题)
前端·javascript·vue.js