解锁ES6+:前端开发的魔法升级

引言

在前端开发的快速发展进程中,JavaScript 作为核心语言,不断推陈出新,其中 ES6 + 特性更是为开发者带来了前所未有的便利和强大功能。从优化变量声明与作用域管理,到革新函数定义与异步编程,再到增强面向对象编程能力和代码模块化组织,ES6 + 特性全方位地提升了前端开发的效率、可读性和可维护性。无论是构建复杂的单页应用,还是打造高性能的 Web 应用程序,ES6 + 特性都已成为现代前端开发不可或缺的基石。

ES6 + 基础特性速览

let 与 const:变量声明的新姿势

在 ES6 之前,JavaScript 主要使用var关键字来声明变量,然而var存在函数作用域和变量提升等问题,容易导致一些难以排查的错误。例如:

js 复制代码
// 使用var声明变量

function varTest() {
	var a = 1;
	if (true) {
		var a = 2;
		console.log(a); // 输出2,因为var声明的变量在函数内作用域是相同的
	}
	console.log(a); // 输出2,同样是因为var的函数作用域特性
}

从 ES6 开始,let和const的出现很好地解决了这些问题。let声明的变量具有块级作用域,只在其所在的代码块内有效,不存在变量提升,并且不能在同一作用域内重复声明。例如:

js 复制代码
// 使用let声明变量
function letTest() {
	let a = 1;
	if (true) {
		let a = 2;
		console.log(a); // 输出2,因为let声明的变量具有块级作用域
	}
	console.log(a); // 输出1,这里访问的是外层作用域的a
}

const用于声明常量,一旦声明,其值就不能被改变,同样具有块级作用域,且必须在声明时初始化。比如:

js 复制代码
// 使用const声明常量
const PI = 3.14159;
// PI = 3.14; // 这会报错,因为常量不能被重新赋值

let和const的块级作用域特性还能避免内层变量覆盖外层变量的问题,以及循环变量泄露为全局变量的情况,让代码的逻辑更加清晰,可维护性更强。

箭头函数:简洁至上的函数表达

箭头函数是 ES6 引入的一种更简洁的函数表达方式,它摒弃了传统函数的复杂语法,让代码更加简洁明了。例如,传统函数定义一个简单的加法函数如下:

js 复制代码
// 传统函数定义加法函数
function add(x, y) {
	return x + y;
}

使用箭头函数可以写成:

js 复制代码
// 箭头函数定义加法函数
const add = (x, y) => x + y;

箭头函数不仅语法简洁,在this绑定上也有独特的规则。它没有自己的this,而是捕获其所在上下文的this值,这使得在处理回调函数时,this的指向更加可预测,避免了传统函数中常见的this指向混乱问题。例如:

js 复制代码
const obj = {
    data: [1, 2, 3],
    sum: function() {
        return this.data.reduce((acc, num) => acc + num, 0);
    }
};
console.log(obj.sum()); // 输出6,这里箭头函数中的this指向obj

然而,箭头函数也有其局限性,它不能作为构造函数使用,没有arguments对象,不适合需要动态改变this指向的场景。所以在实际使用中,要根据具体需求合理选择。

模板字符串:告别繁琐的字符串拼接

在 ES6 之前,拼接字符串是一件繁琐的事情,需要使用大量的+运算符,代码可读性较差。例如:

js 复制代码
// ES6之前的字符串拼接
const name = "Alice";
const age = 25;
const message = "My name is " + name + ". I'm " + age + " years old.";
console.log(message);

ES6 引入的模板字符串使用反引号(`)来定义,并且可以在其中嵌入变量和表达式,极大地简化了字符串拼接操作。例如:

js 复制代码
// 使用模板字符串
const name = "Alice";
const age = 25;
const message = `My name is ${name}. I'm ${age} years old.`;
console.log(message);

模板字符串还能轻松处理多行字符串,无需使用转义字符。例如:

js 复制代码
// 多行字符串
const html = `
    <div>
        <p>这是一段多行的HTML代码</p>
        <p>使用模板字符串处理非常方便</p>
    </div>
`;
console.log(html);

此外,模板字符串还支持标签函数,通过标签函数可以对模板字符串进行更灵活的处理,比如实现字符串的格式化、安全过滤等功能。

数据结构与操作的革新

解构赋值:轻松提取数据

解构赋值是 ES6 中一种强大的语法,它允许我们从数组和对象中提取数据,并将其赋值给变量,大大简化了数据提取的过程。

在数组解构中,我们可以按照位置匹配的方式提取数组元素。例如:

js 复制代码
// 数组解构
const numbers = [1, 2, 3];
const [a, b, c] = numbers;
console.log(a); // 输出1
console.log(b); // 输出2
console.log(c); // 输出3

还可以使用默认值,当解构的位置没有对应元素时,会使用默认值。比如:

js 复制代码
const [x, y = 10] = [1];
console.log(x); // 输出1
console.log(y); // 输出10,因为数组中没有第二个元素,所以使用默认值

对于对象解构,是通过属性名进行匹配赋值的。例如:

js 复制代码
// 对象解构
const user = { name: "Bob", age: 30, email: "bob@example.com" };
const { name, age, email } = user;
console.log(name); // 输出Bob
console.log(age); // 输出30
console.log(email); // 输出bob@example.com

也能进行嵌套对象的解构,以及更改属性名。例如:

js 复制代码
const user = {
    name: "Alice",
    address: {
        city: "New York",
        zip: "10001"
    }
};
// 嵌套对象解构
const { name, address: { city } } = user;
console.log(name); // 输出Alice
console.log(city); // 输出New York
// 更改属性名
const { name: userName, age: userAge } = user;
console.log(userName); // 输出Alice
console.log(userAge); // 这里user中没有age属性,所以会报错

解构赋值在函数参数中也非常实用,可以使函数参数的传递和处理更加清晰。例如:

js 复制代码
function printUser({ name, age }) {
    console.log(`Name: ${name}, Age: ${age}`);
}
const user = { name: "Charlie", age: 25 };
printUser(user); // 输出Name: Charlie, Age: 25

扩展运算符与剩余运算符:灵活的数据处理

扩展运算符(...)和剩余运算符(...)是 ES6 中用于灵活处理数据的重要工具,它们在数组和对象操作中发挥着关键作用。

扩展运算符可以将数组或对象展开成一系列值,在数组操作中,常用于合并数组、复制数组等。例如:

js 复制代码
// 合并数组
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combinedArr = [...arr1,...arr2];
console.log(combinedArr); // 输出[1, 2, 3, 4, 5, 6]
// 复制数组
const originalArr = [1, 2, 3];
const copiedArr = [...originalArr];
console.log(copiedArr); // 输出[1, 2, 3]

在对象操作中,扩展运算符可以用于合并对象、克隆对象等。例如:

js 复制代码
// 合并对象
const obj1 = { name: "Alice", age: 25 };
const obj2 = { email: "alice@example.com" };
const mergedObj = {...obj1,...obj2 };
console.log(mergedObj); // 输出{name: "Alice", age: 25, email: "alice@example.com"}
// 克隆对象
const originalObj = { name: "Bob", age: 30 };
const clonedObj = {...originalObj };
console.log(clonedObj); // 输出{name: "Bob", age: 30}

剩余运算符则用于收集多余的参数,将其转换为一个数组,它主要用于函数参数中。例如:

js 复制代码
function sum(...numbers) {
    return numbers.reduce((acc, num) => acc + num, 0);
}
console.log(sum(1, 2, 3, 4, 5)); // 输出15

在这个例子中,...numbers 收集了所有传入的参数,并将它们存储在一个数组中,方便后续处理。

Set 和 Map:新的数据结构

Set 和 Map 是 ES6 引入的两种新的数据结构,它们为 JavaScript 带来了更强大的数据处理能力。

Set 是一种集合数据结构,它的成员是唯一的,没有重复的值,常用于数组去重等场景。例如:

js 复制代码
// 创建Set
const mySet = new Set();
mySet.add(1);
mySet.add(2);
mySet.add(2); // 重复添加,不会生效
console.log(mySet.size); // 输出2
// 数组去重
const numbers = [1, 2, 2, 3, 3, 3];
const uniqueNumbers = [...new Set(numbers)];
console.log(uniqueNumbers); // 输出[1, 2, 3]

Set 还提供了一些方法,如has用于检查是否包含某个元素,delete用于删除元素,clear用于清空集合等。

Map 是一种键值对的集合,与普通对象不同,它的键可以是任意类型的数据,并且保留了键值对的插入顺序。例如:

js 复制代码
// 创建Map
const myMap = new Map();
myMap.set("name", "Alice");
myMap.set(1, "One");
const objKey = {};
myMap.set(objKey, "Value for object key");
console.log(myMap.get("name")); // 输出Alice
console.log(myMap.get(1)); // 输出One
console.log(myMap.get(objKey)); // 输出Value for object key

Map 也有一系列方法,如has检查是否包含某个键,delete删除键值对,clear清空 Map,size获取键值对数量等,还可以通过keys、values、entries方法分别获取键、值、键值对的迭代器,方便进行遍历操作。

异步编程的进化

Promise:告别回调地狱

在 JavaScript 的异步编程发展历程中,Promise 的出现堪称一次重大变革。在 Promise 之前,异步操作主要通过回调函数来实现,这在简单场景下尚可应付,但当异步操作变得复杂,尤其是存在多个嵌套的异步调用时,就会陷入令人头疼的 "回调地狱"。例如,在进行一个需要依次获取用户信息、用户订单,再根据订单获取商品详情的操作时,使用回调函数的代码可能如下:

js 复制代码
// 模拟异步获取用户信息
function getUserInfo(callback) {
    setTimeout(() => {
        const userInfo = { id: 1, name: "Alice" };
        callback(null, userInfo);
    }, 1000);
}
// 模拟异步获取用户订单
function getUserOrders(userInfo, callback) {
    setTimeout(() => {
        const orders = [{ id: 101, product: "Book" }, { id: 102, product: "Pen" }];
        callback(null, orders);
    }, 1000);
}
// 模拟异步获取商品详情
function getProductDetails(order, callback) {
    setTimeout(() => {
        const productDetails = { id: order.id, details: "This is a " + order.product };
        callback(null, productDetails);
    }, 1000);
}
getUserInfo((err, userInfo) => {
    if (err) return console.error(err);
    getUserOrders(userInfo, (err, orders) => {
        if (err) return console.error(err);
        orders.forEach(order => {
            getProductDetails(order, (err, productDetails) => {
                if (err) return console.error(err);
                console.log(productDetails);
            });
        });
    });
});

这段代码不仅嵌套层次深,而且阅读和维护难度极大,一旦中间某个异步操作出现问题,调试起来非常困难。

Promise 的出现有效地解决了这个问题。Promise 是一个代表异步操作最终完成或失败的对象,它有三种状态:pending(进行中)、fulfilled(已完成)和rejected(已拒绝)。使用 Promise,上述代码可以改写为:

js 复制代码
// 模拟异步获取用户信息
function getUserInfo() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const userInfo = { id: 1, name: "Alice" };
            resolve(userInfo);
        }, 1000);
    });
}
// 模拟异步获取用户订单
function getUserOrders(userInfo) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const orders = [{ id: 101, product: "Book" }, { id: 102, product: "Pen" }];
            resolve(orders);
        }, 1000);
    });
}
// 模拟异步获取商品详情
function getProductDetails(order) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const productDetails = { id: order.id, details: "This is a " + order.product };
            resolve(productDetails);
        }, 1000);
    });
}
getUserInfo()
  .then(userInfo => getUserOrders(userInfo))
  .then(orders => {
        const promises = orders.map(order => getProductDetails(order));
        return Promise.all(promises);
    })
  .then(productDetailsList => {
        productDetailsList.forEach(productDetails => {
            console.log(productDetails);
        });
    })
  .catch(err => console.error(err));

在这段代码中,通过then方法来处理 Promise 成功后的结果,通过catch方法来捕获 Promise 链中任何一个环节出现的错误,使得异步操作的流程更加清晰,代码的可读性和可维护性大大提高。同时,Promise 还支持链式调用,使得多个异步操作可以按照顺序依次执行,避免了回调函数的层层嵌套。

async/await:异步操作的语法糖

async/await是 ES7 引入的异步编程语法,它基于 Promise,是对 Promise 的进一步优化,被称为异步操作的 "语法糖"。async用于声明一个异步函数,该函数返回一个 Promise 对象;await只能在async函数内部使用,用于等待一个 Promise 对象的解决,它会暂停async函数的执行,直到 Promise 对象被解决(resolved)或拒绝(rejected),然后恢复async函数的执行,并返回 Promise 对象的值。

使用async/await,上述获取用户信息、订单和商品详情的代码可以进一步简化为:

js 复制代码
// 模拟异步获取用户信息
function getUserInfo() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const userInfo = { id: 1, name: "Alice" };
            resolve(userInfo);
        }, 1000);
    });
}
// 模拟异步获取用户订单
function getUserOrders(userInfo) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const orders = [{ id: 101, product: "Book" }, { id: 102, product: "Pen" }];
            resolve(orders);
        }, 1000);
    });
}
// 模拟异步获取商品详情
function getProductDetails(order) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const productDetails = { id: order.id, details: "This is a " + order.product };
            resolve(productDetails);
        }, 1000);
    });
}
async function main() {
    try {
        const userInfo = await getUserInfo();
        const orders = await getUserOrders(userInfo);
        const productDetailsList = await Promise.all(orders.map(order => getProductDetails(order)));
        productDetailsList.forEach(productDetails => {
            console.log(productDetails);
        });
    } catch (err) {
        console.error(err);
    }
}
main();

在这段代码中,async/await让异步代码看起来更像同步代码,极大地提高了代码的可读性和可维护性。错误处理也变得更加直观,通过try/catch块可以捕获await操作中可能出现的错误,而不需要像 Promise 链式调用那样在每个then方法后面添加catch方法。同时,async/await还可以与Promise.all、Promise.race等方法结合使用,更加灵活地处理复杂的异步操作场景。

模块化与面向对象编程的加强

模块化:代码组织的新方式

在 JavaScript 的发展历程中,模块化一直是一个重要的议题。在 ES6 之前,JavaScript 缺乏原生的模块化支持,开发者们只能依赖于各种第三方库和工具来实现模块化,如 CommonJS(主要用于 Node.js 环境)和 AMD(主要用于浏览器环境)。这些方案虽然在一定程度上解决了模块化的问题,但存在一些局限性,例如在浏览器中使用 CommonJS 需要额外的工具进行转换,AMD 的语法相对复杂等。

ES6 引入的模块系统为 JavaScript 带来了原生的模块化支持,它使用export和import关键字来实现模块的导出和导入,使得代码的组织和管理更加方便和直观。

在一个模块中,可以使用export关键字将需要暴露给其他模块使用的变量、函数、类等导出。例如,创建一个mathUtils.js模块,其中定义了一些数学运算函数并导出:

js 复制代码
// mathUtils.js
// 导出一个加法函数
export function add(a, b) {
    return a + b;
}
// 导出一个乘法函数
export function multiply(a, b) {
    return a * b;
}

在另一个模块中,可以使用import关键字导入mathUtils.js模块中导出的函数:

js 复制代码
// main.js
import { add, multiply } from './mathUtils.js';
console.log(add(3, 5)); // 输出8
console.log(multiply(4, 6)); // 输出24

这里使用了具名导入,通过大括号指定要导入的具体内容。如果模块中只有一个主要的导出内容,还可以使用默认导出和默认导入。例如,创建一个message.js模块,使用默认导出一个函数:

js 复制代码
// message.js
// 默认导出一个函数
export default function greet(name) {
    return `Hello, ${name}!`;
}

在导入时,不需要使用大括号,直接指定导入的名称即可:

js 复制代码
// main.js
import greet from './message.js';
console.log(greet('Alice')); // 输出Hello, Alice!

此外,还可以使用import * as的方式将模块中的所有导出内容导入到一个对象中,方便统一管理和使用:

js 复制代码
// main.js
import * as math from './mathUtils.js';
console.log(math.add(2, 3)); // 输出5
console.log(math.multiply(5, 7)); // 输出35

ES6 模块系统还支持动态导入,通过import()函数实现按需加载模块,这在一些场景下可以提高应用的性能,例如在路由懒加载中,只有当用户访问到特定页面时才加载对应的模块代码。

类:面向对象编程的升级

在 ES6 之前,JavaScript 主要通过构造函数和原型链来实现面向对象编程,这种方式虽然灵活,但语法相对复杂,容易让开发者感到困惑。例如,使用构造函数创建一个简单的Person对象:

js 复制代码
// 使用构造函数创建Person对象
function Person(name, age) {
    this.name = name;
    this.age = age;
}
// 为Person对象的原型添加方法
Person.prototype.sayHello = function() {
    console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
};
const person = new Person('Bob', 30);
person.sayHello(); // 输出Hello, my name is Bob and I'm 30 years old.

ES6 引入的class关键字为 JavaScript 的面向对象编程带来了更简洁、直观的语法,它是基于原型链的面向对象编程的语法糖,使得代码更易于理解和维护。使用class定义一个Person类:

js 复制代码
// 使用class定义Person类
class Person {
    // 构造函数,用于初始化对象属性
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    // 类的实例方法
    sayHello() {
        console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
    }
}
const person = new Person('Alice', 25);
person.sayHello(); // 输出Hello, my name is Alice and I'm 25 years old.

在这个例子中,class定义了一个Person类,constructor是类的构造函数,用于初始化对象的属性。sayHello是类的实例方法,直接定义在类的内部,不需要像传统方式那样通过原型来添加。

类的继承在 ES6 中也变得更加简单和直观,使用extends关键字实现类的继承。例如,创建一个Student类继承自Person类:

js 复制代码
// Student类继承自Person类
class Student extends Person {
    constructor(name, age, grade) {
        // 调用父类的构造函数
        super(name, age);
        this.grade = grade;
    }
    // 子类特有的方法
    study() {
        console.log(`${this.name} is studying in grade ${this.grade}.`);
    }
}
const student = new Student('Charlie', 18, 12);
student.sayHello(); // 输出Hello, my name is Charlie and I'm 18 years old.
student.study(); // 输出Charlie is studying in grade 12.

在这个例子中,Student类通过extends关键字继承了Person类的属性和方法,super关键字用于调用父类的构造函数,确保父类的属性被正确初始化。study方法是Student类特有的方法,展示了子类对父类的扩展。

此外,ES6 类还支持静态方法,通过在方法前加上static关键字来定义,静态方法可以直接通过类名调用,而不需要创建类的实例。例如:

js 复制代码
class MathUtils {
    static add(a, b) {
        return a + b;
    }
}
console.log(MathUtils.add(3, 5)); // 输出8

通过 ES6 的class语法,JavaScript 的面向对象编程变得更加简洁、清晰,提高了代码的可读性和可维护性,也更符合现代面向对象编程的习惯。

实践与应用案例

在实际的前端项目开发中,ES6 + 特性发挥着至关重要的作用,尤其是在 React 和 Vue 这两个主流的前端框架开发中。

React 开发中的 ES6 + 应用

在 React 开发中,ES6 + 的类语法让组件的定义更加简洁和直观。例如,使用 ES6 类来定义一个 React 组件:

js 复制代码
import React, { Component } from'react';
class MyComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        };
    }
    increment = () => {
        this.setState(prevState => ({
            count: prevState.count + 1
        }));
    }
    render() {
        return (
            <div>
                <p>Count: {this.state.count}</p>
                <button onClick={this.increment}>Increment</button>
            </div>
        );
    }
}
export default MyComponent;

在这个例子中,使用class定义组件,constructor方法用于初始化组件的状态,increment方法使用箭头函数定义,避免了手动绑定this的问题。同时,this.setState使用了函数式更新的方式,确保在状态依赖于前一个状态时能正确更新。

解构赋值在 React 中也经常用于提取props和state中的数据。例如:

js 复制代码
import React, { Component } from'react';
class UserInfo extends Component {
    constructor(props) {
        super(props);
        this.state = {
            user: {
                name: "Alice",
                age: 25,
                email: "alice@example.com"
            }
        };
    }
    render() {
        const { name, age } = this.state.user;
        return (
            <div>
                <p>Name: {name}</p>
                <p>Age: {age}</p>
            </div>
        );
    }
}
export default UserInfo;

这里通过解构赋值从this.state.user中提取出name和age,使代码更加简洁,可读性更强。

Vue 开发中的 ES6 + 应用

在 Vue 开发中,ES6 的模块化特性使得代码的组织和管理更加方便。例如,在一个 Vue 项目中,我们可以将不同的功能模块分别封装在不同的文件中,然后通过import和export进行导入和导出。

假设我们有一个utils.js文件,定义了一些工具函数:

js 复制代码
// utils.js
export function formatDate(date) {
    return date.toISOString().split('T')[0];
}
export function capitalize(str) {
    return str.charAt(0).toUpperCase() + str.slice(1);
}

在 Vue 组件中使用这些函数:

js 复制代码
<template>
    <div>
        <p>Formatted Date: {{ formattedDate }}</p>
        <p>Capitalized Name: {{ capitalizedName }}</p>
    </div>
</template>
<script>
import { formatDate, capitalize } from './utils.js';
export default {
    data() {
        return {
            originalDate: new Date(),
            originalName: "alice"
        };
    },
    computed: {
        formattedDate() {
            return formatDate(this.originalDate);
        },
        capitalizedName() {
            return capitalize(this.originalName);
        }
    }
};
</script>

在这个例子中,通过import导入utils.js中的函数,在组件的computed属性中使用这些函数对数据进行处理,使代码结构更加清晰,逻辑更加明确。

在 Vue 的methods中,也经常使用箭头函数来简化代码。例如:

js 复制代码
<template>
    <div>
        <button @click="increment">Increment</button>
        <p>Count: {{ count }}</p>
    </div>
</template>
<script>
export default {
    data() {
        return {
            count: 0
        };
    },
    methods: {
        increment: () => {
            // 这里需要注意,箭头函数中的this不是指向Vue实例,
            // 通常用于一些不依赖于Vue实例的回调函数场景
            console.log('Increment button clicked');
        }
    }
};
</script>

虽然在这个例子中,箭头函数在methods中的使用有一定局限性(因为this指向问题),但在一些特定场景,如处理外部库的回调函数时,箭头函数可以提供简洁的语法和明确的this指向控制。

总结与展望

ES6 + 特性为 JavaScript 带来了全方位的升级,从基础语法到异步编程,从数据结构到模块化和面向对象编程,每一个方面都得到了显著的改进和增强。这些特性不仅提升了代码的可读性、可维护性和开发效率,还使得 JavaScript 能够更好地应对复杂的前端开发需求。在 React 和 Vue 等主流前端框架开发中,ES6 + 特性更是发挥了关键作用,成为现代前端开发不可或缺的组成部分。

随着技术的不断发展,JavaScript 还将持续演进,未来有望在性能优化、与其他技术的融合(如 WebAssembly、人工智能等)以及语言特性的进一步完善等方面取得更大的突破。作为前端开发者,我们应紧跟技术发展的步伐,不断学习和掌握新的特性和技能,充分利用 ES6 + 以及未来 JavaScript 的新特性,打造出更加高效、优质的前端应用。

相关推荐
加班是不可能的,除非双倍日工资4 小时前
css预编译器实现星空背景图
前端·css·vue3
wyiyiyi5 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip5 小时前
vite和webpack打包结构控制
前端·javascript
excel5 小时前
在二维 Canvas 中模拟三角形绕 X、Y 轴旋转
前端
阿华的代码王国6 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
一条上岸小咸鱼6 小时前
Kotlin 基本数据类型(三):Booleans、Characters
android·前端·kotlin
Jimmy6 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
ZXT6 小时前
promise & async await总结
前端
Jerry说前后端6 小时前
RecyclerView 性能优化:从原理到实践的深度优化方案
android·前端·性能优化
画个太阳作晴天6 小时前
A12预装app
linux·服务器·前端