跳到主要内容SpringBoot + Vue 前后端分离架构实战:权限、工作流与报表 | 极客日志Java大前端java
SpringBoot + Vue 前后端分离架构实战:权限、工作流与报表
基于 SpringBoot 与 Vue 构建的前后端分离企业级管理系统,涵盖 RBAC 权限模型、Flowable 工作流引擎及动态报表功能。通过 Spring Security + JWT 实现细粒度接口控制,利用 BPMN 2.0 设计可视化审批流程,结合 ECharts 与 MyBatis-Plus 完成数据可视化展示。项目提供 Docker 容器化部署方案,适合中大型业务场景参考。
女王1 浏览 SpringBoot + Vue 前后端分离架构实战:权限、工作流与报表
前后端分离已成为企业级应用的主流。本文将通过一个完整的企业管理系统实战,详细介绍如何使用 SpringBoot + Vue 技术栈,实现权限管理、工作流引擎和报表系统三大核心功能。
一、项目背景与技术选型
1.1 技术栈总览
| 层次 | 技术选型 | 说明 |
|---|
| 前端框架 | Vue 3 + TypeScript | 渐进式框架 |
| UI 组件库 | Element Plus | 企业级组件库 |
| 状态管理 | Pinia | Vue 3 官方推荐 |
| 后端框架 | Spring Boot 2.7 | Java 微服务框架 |
| ORM 框架 | MyBatis-Plus | 持久层增强 |
| 数据库 | MySQL 8.0 | 关系型数据库 |
| 缓存 | Redis 6.0 | 高性能缓存 |
| 工作流引擎 | Flowable 7.0 | 开源 BPMN 平台 |
| 报表工具 | ECharts + UReport2 | 数据可视化 |
1.2 项目结构
enterprise-system/
├── enterprise-admin/
│ ├── src/
│ │ ├── api/
│ │ ├── assets/
│ │ ├── components/
│ │ ├── views/
│ │ ├── router/
│ │ ├── store/
│ │ └── utils/
│ └── package.json
├── enterprise-server/
│ ├── src/main/java/com/enterprise/
│ │ ├── controller/
│ │ ├── service/
│ │ ├── mapper/
│ │ ├── entity/
│ │ ├── dto/
│ │ ├── config/
│ │ └── security/
│ └── pom.xml
└── docs/
二、系统架构设计
2.1 整体架构
系统采用分层架构,从下至上依次为数据层、应用层、网关层和前端层。
- 数据层:MySQL 存储业务数据,Redis 处理缓存,Flowable 独立部署流程库。
- 应用层:包含认证、用户、权限、工作流及报表服务。
- 网关层:Nginx 反向代理,统一入口。
- 前端层:Vue 3 应用,Element Plus UI,Pinia 状态管理。
2.2 数据库设计
- USER: 用户表 (id, username, password, email, phone)
- ROLE: 角色表 (id, role_name, role_code)
- PERMISSION: 权限表 (id, permission_name, resource_type, permission_code)
- WORKFLOW_INSTANCE: 流程实例表 (id, instance_id, definition_id, status)
- WORKFLOW_DEFINITION: 流程定义表 (id, process_key, bpmn_xml)
三、权限管理模块
3.1 RBAC 权限模型
我们采用经典的 RBAC(Role-Based Access Control)模型。用户关联角色,角色关联权限,权限对应具体资源(菜单、按钮或接口)。这种设计能灵活应对细粒度的权限控制需求。
3.2 Spring Security + JWT 实现
在安全配置这块,我们主要关注 JWT 的集成。下面看看具体的配置类怎么写。
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Autowired
private UserDetailsService jwtUserDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers("/api/auth/**", "/api/public/**", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated();
httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
}
这里禁用了 CSRF,因为前后端分离通常使用 Token 认证。JWT 工具类负责生成和验证令牌,注意密钥管理和过期时间设置。
@Component
public class JwtTokenUtil {
private static final String SECRET = "enterprise-secret-key-2024";
private static final long EXPIRATION = 86400000;
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("username", userDetails.getUsername());
return createToken(claims, userDetails.getUsername());
}
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
public String extractUsername(String token) {
return extractClaims(token).getSubject();
}
private Claims extractClaims(String token) {
return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
}
private Boolean isTokenExpired(String token) {
return extractClaims(token).getExpiration().before(new Date());
}
}
在 Controller 中,我们可以利用注解直接控制方法级别的权限。
@RestController
@RequestMapping("/api/user")
public class UserController {
@PreAuthorize("hasRole('USER')")
@GetMapping("/profile")
public Result<UserProfile> getProfile() {
return Result.success(userService.getProfile());
}
@PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
@PutMapping("/{id}")
public Result<Void> updateUser(@PathVariable Long id, @RequestBody UserDTO userDTO) {
userService.updateUser(id, userDTO);
return Result.success();
}
}
3.3 前端权限控制
前端主要通过路由守卫和自定义指令来拦截未授权请求。
路由守卫
import router from './index'
import { useUserStore } from '@/store/user'
import { getToken } from '@/utils/auth'
import { ElMessage } from 'element-plus'
const whiteList = ['/login', '/404', '/403']
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore()
const hasToken = getToken()
if (hasToken) {
if (to.path === '/login') {
next({ path: '/' })
} else {
if (userStore.userId) {
next()
} else {
try {
await userStore.getUserInfo()
await userStore.generateRoutes()
next({ ...to, replace: true })
} catch (error) {
await userStore.logout()
ElMessage.error('登录状态已过期,请重新登录')
next(`/login?redirect=${to.path}`)
}
}
}
} else {
if (whiteList.includes(to.path)) {
next()
} else {
next(`/login?redirect=${to.path}`)
}
}
})
自定义权限指令
import type { Directive, DirectiveBinding } from 'vue'
import { useUserStore } from '@/store/user'
const permission: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const { value } = binding
const userStore = useUserStore()
const permissions = userStore.permissions
if (value && value instanceof Array && value.length > 0) {
const hasPermission = permissions.some((permission: string) => {
return value.includes(permission)
})
if (!hasPermission) {
el.parentNode?.removeChild(el)
}
} else {
throw new Error('需要权限!如 v-permission="[\'user:add\']"')
}
}
}
export default permission
<template>
<div>
<el-button v-permission="['user:delete']" type="danger" @click="handleDelete"> 删除 </el-button>
<el-button v-permission="['user:edit', 'user:audit']" type="primary" @click="handleEdit"> 编辑 </el-button>
</div>
</template>
四、工作流引擎集成
4.1 Flowable 集成配置
引入 Maven 依赖后,需要在配置文件中指定数据库类型和异步执行器。
flowable:
database-schema-update: true
database-type: mysql
async-executor-activate: true
async-history-enabled: true
process:
definition-cache-limit: 100
4.2 流程定义服务
@Service
public class WorkflowService {
@Autowired
private ProcessEngine processEngine;
@Autowired
private RuntimeService runtimeService;
@Autowired
private TaskService taskService;
@Autowired
private HistoryService historyService;
public String deployProcess(String processName, MultipartFile bpmnFile) {
Deployment deployment = processEngine.getRepositoryService()
.createDeployment()
.name(processName)
.addInputStream(bpmnFile.getOriginalFilename(), bpmnFile.getInputStream())
.deploy();
return deployment.getId();
}
public String startProcess(String processKey, Map<String, Object> variables) {
ProcessInstance instance = runtimeService.startProcessInstanceByKey(processKey, variables);
return instance.getId();
}
public void completeTask(String taskId, Map<String, Object> variables) {
taskService.complete(taskId, variables);
}
public List<TaskDTO> getUserTasks(String userId) {
List<Task> tasks = taskService.createTaskQuery()
.taskAssignee(userId)
.orderByTaskCreateTime()
.desc()
.list();
return tasks.stream().map(this::convertToDTO).collect(Collectors.toList());
}
}
4.3 请假审批流程示例
这是一个典型的 BPMN 2.0 流程定义,包含了排他网关来判断天数,从而决定审批层级。
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
targetNamespace="Examples">
<process id="leaveRequest" name="请假审批流程" isExecutable="true">
<startEvent id="startEvent" name="提交申请"/>
<userTask id="fillLeaveForm" name="填写请假单" flowable:assignee="${applicant}"/>
<exclusiveGateway id="checkDaysGateway" name="判断天数"/>
<userTask id="managerApproval" name="部门主管审批" flowable:candidateGroups="MANAGER"/>
<userTask id="directorApproval" name="总监审批" flowable:candidateGroups="DIRECTOR"/>
<userTask id="ceoApproval" name="总经理审批" flowable:candidateGroups="CEO"/>
<exclusiveGateway id="approvalResultGateway" name="审批结果"/>
<serviceTask id="notifyApproved" name="发送通过通知" flowable:class="com.enterprise.workflow.delegate.ApprovedNotifyDelegate"/>
<serviceTask id="notifyRejected" name="发送拒绝通知" flowable:class="com.enterprise.workflow.delegate.RejectedNotifyDelegate"/>
<endEvent id="endEvent" name="流程结束"/>
<sequenceFlow sourceRef="startEvent" targetRef="fillLeaveForm"/>
<sequenceFlow sourceRef="fillLeaveForm" targetRef="checkDaysGateway"/>
<sequenceFlow sourceRef="checkDaysGateway" targetRef="managerApproval">
<conditionExpression xsi:type="tFormalExpression"> ${leaveDays <= 3} </conditionExpression>
</sequenceFlow>
<sequenceFlow sourceRef="checkDaysGateway" targetRef="directorApproval">
<conditionExpression xsi:type="tFormalExpression"> ${leaveDays > 3 && leaveDays <= 7} </conditionExpression>
</sequenceFlow>
<sequenceFlow sourceRef="checkDaysGateway" targetRef="ceoApproval">
<conditionExpression xsi:type="tFormalExpression"> ${leaveDays > 7} </conditionExpression>
</sequenceFlow>
<sequenceFlow sourceRef="managerApproval" targetRef="approvalResultGateway"/>
<sequenceFlow sourceRef="directorApproval" targetRef="approvalResultGateway"/>
<sequenceFlow sourceRef="ceoApproval" targetRef="approvalResultGateway"/>
<sequenceFlow sourceRef="approvalResultGateway" targetRef="notifyApproved">
<conditionExpression xsi:type="tFormalExpression"> ${approved == true} </conditionExpression>
</sequenceFlow>
<sequenceFlow sourceRef="approvalResultGateway" targetRef="notifyRejected">
<conditionExpression xsi:type="tFormalExpression"> ${approved == false} </conditionExpression>
</sequenceFlow>
<sequenceFlow sourceRef="notifyApproved" targetRef="endEvent"/>
<sequenceFlow sourceRef="notifyRejected" targetRef="endEvent"/>
</process>
</definitions>
4.4 前端流程图组件
前端使用 bpmn-js 渲染流程图,并高亮当前活动节点。
<template>
<div>
<div ref="bpmnCanvas"></div>
<div>
<el-tag>当前环节:{{ currentTaskName }}</el-tag>
<el-tag type="success">处理人:{{ currentAssignee }}</el-tag>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import BpmnJS from 'bpmn-js/lib/NavigatedViewer'
import 'bpmn-js/dist/assets/diagram-js.css'
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css'
interface Props {
processInstanceId: string
xml: string
activeActivityIds: string[]
}
const props = defineProps<Props>()
const bpmnCanvas = ref<HTMLElement>()
const currentTaskName = ref('')
const currentAssignee = ref('')
let bpmnViewer: BpmnJS | null = null
onMounted(async () => {
if (bpmnCanvas.value) {
bpmnViewer = new BpmnJS({ container: bpmnCanvas.value })
try {
await bpmnViewer.importXML(props.xml)
const canvas = bpmnViewer.get('canvas')
const elementRegistry = bpmnViewer.get('elementRegistry')
props.activeActivityIds.forEach(id => {
const element = elementRegistry.get(id)
if (element) {
canvas.addMarker(id, 'highlight')
}
})
} catch (error) {
console.error('流程图渲染失败:', error)
}
}
})
</script>
<style>
.bpmn-canvas {
height: 600px;
border: 1px solid #ddd;
}
.highlight {
fill: #67C23A !important;
stroke: #67C23A !important;
}
</style>
五、报表系统实现
5.1 动态报表服务
支持动态 SQL 查询和多种图表类型(柱状图、折线图、饼图)。
@Service
public class ReportService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private ReportConfigMapper reportConfigMapper;
public List<Map<String, Object>> executeQuery(Long reportId, Map<String, Object> params) {
ReportConfig config = reportConfigMapper.selectById(reportId);
String sql = replaceParams(config.getSqlTemplate(), params);
return jdbcTemplate.queryForList(sql);
}
public ChartDataVO generateChartData(Long reportId, Map<String, Object> params) {
ReportConfig config = reportConfigMapper.selectById(reportId);
List<Map<String, Object>> data = executeQuery(reportId, params);
ChartDataVO chartData = new ChartDataVO();
chartData.setType(config.getChartType());
if ("bar".equals(config.getChartType()) || "line".equals(config.getChartType())) {
List<String> xAxis = new ArrayList<>();
List<BigDecimal> yAxis = new ArrayList<>();
for (Map<String, Object> row : data) {
xAxis.add(row.get(config.getXAxisField()).toString());
yAxis.add((BigDecimal) row.get(config.getYAxisField()));
}
chartData.setXAxis(xAxis);
chartData.setSeries(Collections.singletonList(new SeriesVO("数值", yAxis)));
} else if ("pie".equals(config.getChartType())) {
List<PieDataVO> pieData = new ArrayList<>();
for (Map<String, Object> row : data) {
pieData.add(new PieDataVO(
row.get(config.getNameField()).toString(),
((BigDecimal) row.get(config.getValueField())).doubleValue()
));
}
chartData.setData(pieData);
}
return chartData;
}
public void exportReport(Long reportId, Map<String, Object> params, HttpServletResponse response) throws IOException {
List<Map<String, Object>> data = executeQuery(reportId, params);
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
response.setHeader("Content-disposition", "attachment;filename=report.xlsx");
EasyExcel.write(response.getOutputStream()).sheet("报表数据").doWrite(data);
}
private String replaceParams(String template, Map<String, Object> params) {
String result = template;
for (Map.Entry<String, Object> entry : params.entrySet()) {
result = result.replace("${" + entry.getKey() + "}", String.valueOf(entry.getValue()));
}
return result;
}
}
5.2 前端报表组件
前端通过 ECharts 渲染图表,并提供导出 Excel 功能。
<template>
<div>
<el-form :model="queryParams" inline>
<el-form-item v-for="param in paramList" :key="param.name" :label="param.label">
<el-input v-if="param.type === 'input'" v-model="queryParams[param.name]" />
<el-date-picker v-else-if="param.type === 'date'" v-model="queryParams[param.name]" type="daterange" />
<el-select v-else-if="param.type === 'select'" v-model="queryParams[param.name]">
<el-option v-for="opt in param.options" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadReport">查询</el-button>
<el-button @click="exportReport">导出</el-button>
</el-form-item>
</el-form>
<div v-if="reportConfig.reportType === 'chart'" ref="chartRef"></div>
<el-table v-else :data="tableData" border>
<el-table-column v-for="col in columns" :key="col.prop" :prop="col.prop" :label="col.label" />
</el-table>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
import { getReportData, exportReportData } from '@/api/report'
interface Props {
reportId: number
}
const props = defineProps<Props>()
const chartRef = ref<HTMLElement>()
const chartInstance = ref<echarts.ECharts>()
const queryParams = ref<Record<string, any>>({})
const paramList = ref<any[]>([])
const reportConfig = ref<any>({})
const tableData = ref<any[]>([])
const columns = ref<any[]>([])
onMounted(async () => {
const config = await getReportConfig(props.reportId)
reportConfig.value = config
paramList.value = JSON.parse(config.paramConfig || '[]')
if (config.reportType === 'chart' && chartRef.value) {
chartInstance.value = echarts.init(chartRef.value)
}
await loadReport()
})
async function loadReport() {
const data = await getReportData(props.reportId, queryParams.value)
if (reportConfig.value.reportType === 'chart') {
renderChart(data)
} else {
tableData.value = data.list
columns.value = data.columns
}
}
function renderChart(data: any) {
const option = generateChartOption(reportConfig.value.chartType, data)
chartInstance.value?.setOption(option)
}
async function exportReport() {
await exportReportData(props.reportId, queryParams.value)
}
</script>
六、核心代码实现
6.1 统一响应格式
所有接口返回统一的 Result 对象,方便前端处理。
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
private Integer code;
private String message;
private T data;
private Long timestamp;
public static <T> Result<T> success(T data) {
return new Result<>(200, "操作成功", data, System.currentTimeMillis());
}
public static <T> Result<T> success() {
return success(null);
}
public static <T> Result<T> error(String message) {
return new Result<>(500, message, null, System.currentTimeMillis());
}
}
6.2 全局异常处理
捕获业务异常、参数校验异常和系统异常,避免将堆栈信息直接暴露给前端。
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e) {
log.error("业务异常:{}", e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidationException(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
log.error("参数校验失败:{}", message);
return Result.error(400, message);
}
@ExceptionHandler(AccessDeniedException.class)
public Result<Void> handleAccessDeniedException(AccessDeniedException e) {
log.error("权限不足:{}", e.getMessage());
return Result.error(403, "权限不足");
}
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
log.error("系统异常", e);
return Result.error("系统错误,请联系管理员");
}
}
6.3 MyBatis-Plus 配置
开启分页插件和乐观锁,自动填充创建时间和更新时间。
@Configuration
@MapperScan("com.enterprise.mapper")
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
@Bean
public MetaObjectHandler metaObjectHandler() {
return new MetaObjectHandler() {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
};
}
}
七、部署与运维
7.1 Docker 容器化部署
使用 Docker Compose 编排 MySQL、Redis、后端和前端服务。
version: '3.8'
services:
mysql:
image: mysql:8.0
container_name: enterprise-mysql
environment:
MYSQL_ROOT_PASSWORD: root123456
MYSQL_DATABASE: enterprise_db
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
- ./sql:/docker-entrypoint-initdb.d
networks:
- enterprise-network
redis:
image: redis:6.2-alpine
container_name: enterprise-redis
ports:
- "6379:6379"
volumes:
- redis-data:/data
networks:
- enterprise-network
backend:
build: ./enterprise-server
container_name: enterprise-backend
ports:
- "8080:8080"
environment:
SPRING_PROFILES_ACTIVE: prod
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/enterprise_db?useSSL=false
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: root123456
SPRING_REDIS_HOST: redis
SPRING_REDIS_PORT: 6379
depends_on:
- mysql
- redis
networks:
- enterprise-network
frontend:
image: nginx:alpine
container_name: enterprise-frontend
ports:
- "80:80"
volumes:
- ./enterprise-admin/dist:/usr/share/nginx/html
- ./nginx.conf:/etc/nginx/conf.d/default.conf
depends_on:
- backend
networks:
- enterprise-network
volumes:
mysql-data:
redis-data:
networks:
enterprise-network:
driver: bridge
7.2 CI/CD 流程
- 开发提交代码至 Gitlab。
- Gitlab 触发构建流水线。
- Maven 构建并运行单元测试。
- 测试通过后构建 Docker 镜像并推送。
- 部署到测试环境进行集成测试。
- 生产环境部署前进行健康检查。
7.3 Nginx 配置
upstream backend {
server backend:8080;
}
server {
listen 80;
server_name your-domain.com;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
client_max_body_size 100M;
gzip on;
gzip_types text/plain text/css application/json application/javascript;
}
八、总结
本项目基于 SpringBoot + Vue 构建,涵盖了企业级应用的核心痛点。
- 权限管理:通过 Spring Security + JWT 实现了细粒度的接口控制。
- 工作流:利用 Flowable BPMN 设计了可视化的审批流程,支持复杂网关逻辑。
- 报表系统:结合 ECharts 与动态 SQL,提供了灵活的数据可视化方案。
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online