寫在開頭
-
本次 TypeScript
一共有15道題,由易道難,能夠說是涵蓋了全部的使用場景,入門容易,精通難 -
以前的上集有8道題,沒有看過的小夥伴,能夠看這以前的文章: -
最近技術團隊會立刻入駐公衆號,後期原創文章會不斷增多,廣告也有,可是你們請理解,這也是創做的動力,大部分收入是會用來發福利,上次就發了一百本書.有的廣告仍是能夠白嫖的課程,點擊進去看看也挺好 -
強烈推薦:15道優秀的TypeScript練習題 (上集)
正式開始
-
第八題,模擬動態返回數據,我使用的是泛型解題,此時不是最優解法, AdminsApiResponse
和DatesApiResponse
能夠進一步封裝抽象成一個接口.有興趣的能夠繼續優化
interface User {
type: 'user';
name: string;
age: number;
occupation: string;
}
interface Admin {
type: 'admin';
name: string;
age: number;
role: string;
}
type responseData = Date;
type Person = User | Admin;
const admins: Admin[] = [
{ type: 'admin', name: 'Jane Doe', age: 32, role: 'Administrator' },
{ type: 'admin', name: 'Bruce Willis', age: 64, role: 'World saver' },
];
const users: User[] = [
{
type: 'user',
name: 'Max Mustermann',
age: 25,
occupation: 'Chimney sweep',
},
{ type: 'user', name: 'Kate Müller', age: 23, occupation: 'Astronaut' },
];
type AdminsApiResponse<T> =
| {
status: 'success';
data: T[];
}
| {
status: 'error';
error: string;
};
type DatesApiResponse<T> =
| {
status: 'success';
data: T;
}
| {
status: 'error';
error: string;
};
function requestAdmins(callback: (response: AdminsApiResponse<Admin>) => void) {
callback({
status: 'success',
data: admins,
});
}
function requestUsers(callback: (response: AdminsApiResponse<User>) => void) {
callback({
status: 'success',
data: users,
});
}
function requestCurrentServerTime(
callback: (response: DatesApiResponse<number>) => void
) {
callback({
status: 'success',
data: Date.now(),
});
}
function requestCoffeeMachineQueueLength(
callback: (response: AdminsApiResponse<User>) => void
) {
callback({
status: 'error',
error: 'Numeric value has exceeded Number.MAX_SAFE_INTEGER.',
});
}
function logPerson(person: Person) {
console.log(
` - ${chalk.green(person.name)}, ${person.age}, ${
person.type === 'admin' ? person.role : person.occupation
}`
);
}
function startTheApp(callback: (error: Error | null) => void) {
requestAdmins((adminsResponse) => {
console.log(chalk.yellow('Admins:'));
if (adminsResponse.status === 'success') {
adminsResponse.data.forEach(logPerson);
} else {
return callback(new Error(adminsResponse.error));
}
console.log();
requestUsers((usersResponse) => {
console.log(chalk.yellow('Users:'));
if (usersResponse.status === 'success') {
usersResponse.data.forEach(logPerson);
} else {
return callback(new Error(usersResponse.error));
}
console.log();
requestCurrentServerTime((serverTimeResponse) => {
console.log(chalk.yellow('Server time:'));
if (serverTimeResponse.status === 'success') {
console.log(
` ${new Date(serverTimeResponse.data).toLocaleString()}`
);
} else {
return callback(new Error(serverTimeResponse.error));
}
console.log();
requestCoffeeMachineQueueLength((coffeeMachineQueueLengthResponse) => {
console.log(chalk.yellow('Coffee machine queue length:'));
if (coffeeMachineQueueLengthResponse.status === 'success') {
console.log(` ${coffeeMachineQueueLengthResponse.data}`);
} else {
return callback(new Error(coffeeMachineQueueLengthResponse.error));
}
callback(null);
});
});
});
});
}
startTheApp((e: Error | null) => {
console.log();
if (e) {
console.log(
`Error: "${e.message}", but it's fine, sometimes errors are inevitable.`
);
} else {
console.log('Success!');
}
});
-
第九題,仍是考察泛型, promisify
的編寫,傳入一個返回promise
的函數,函數接受一個參數,這個參數是一個函數,它有對應的根據泛型生成的參數,返回值爲void
,一樣這個函數的參數也爲函數,返回值也爲void
interface User {
type: 'user';
name: string;
age: number;
occupation: string;
}
interface Admin {
type: 'admin';
name: string;
age: number;
role: string;
}
type Person = User | Admin;
const admins: Admin[] = [
{ type: 'admin', name: 'Jane Doe', age: 32, role: 'Administrator' },
{ type: 'admin', name: 'Bruce Willis', age: 64, role: 'World saver' }
];
const users: User[] = [
{ type: 'user', name: 'Max Mustermann', age: 25, occupation: 'Chimney sweep' },
{ type: 'user', name: 'Kate Müller', age: 23, occupation: 'Astronaut' }
];
type ApiResponse<T> = (
{
status: 'success';
data: T;
} |
{
status: 'error';
error: string;
}
);
function promisify<T>(fn: (callback: (arg: ApiResponse<T>) => void) => void): () => Promise<T>
function promisify(arg: unknown): unknown {
return null;
}
const oldApi = {
requestAdmins(callback: (response: ApiResponse<Admin[]>) => void) {
callback({
status: 'success',
data: admins
});
},
requestUsers(callback: (response: ApiResponse<User[]>) => void) {
callback({
status: 'success',
data: users
});
},
requestCurrentServerTime(callback: (response: ApiResponse<number>) => void) {
callback({
status: 'success',
data: Date.now()
});
},
requestCoffeeMachineQueueLength(callback: (response: ApiResponse<number>) => void) {
callback({
status: 'error',
error: 'Numeric value has exceeded Number.MAX_SAFE_INTEGER.'
});
}
};
const api = {
requestAdmins: promisify(oldApi.requestAdmins),
requestUsers: promisify(oldApi.requestUsers),
requestCurrentServerTime: promisify(oldApi.requestCurrentServerTime),
requestCoffeeMachineQueueLength: promisify(oldApi.requestCoffeeMachineQueueLength)
};
function logPerson(person: Person) {
console.log(
` - ${chalk.green(person.name)}, ${person.age}, ${person.type === 'admin' ? person.role : person.occupation}`
);
}
async function startTheApp() {
console.log(chalk.yellow('Admins:'));
(await api.requestAdmins()).forEach(logPerson);
console.log();
console.log(chalk.yellow('Users:'));
(await api.requestUsers()).forEach(logPerson);
console.log();
console.log(chalk.yellow('Server time:'));
console.log(` ${new Date(await api.requestCurrentServerTime()).toLocaleString()}`);
console.log();
console.log(chalk.yellow('Coffee machine queue length:'));
console.log(` ${await api.requestCoffeeMachineQueueLength()}`);
}
startTheApp().then(
() => {
console.log('Success!');
},
(e: Error) => {
console.log(`Error: "${e.message}", but it's fine, sometimes errors are inevitable.`);
}
);
-
第十題,考察 declaer module
declare module 'str-utils' {
export function strReverse(arg:string): string;
export function strToLower(arg:string): string;
export function strToUpper(arg:string): string;
export function strRandomize(arg:string): string;
export function strInvertCase(arg:string): string;
}
import {
strReverse,
strToLower,
strToUpper,
strRandomize,
strInvertCase
} from 'str-utils';
interface User {
type: 'user';
name: string;
age: number;
occupation: string;
}
interface Admin {
type: 'admin';
name: string;
age: number;
role: string;
}
type Person = User | Admin;
const admins: Admin[] = [
{ type: 'admin', name: 'Jane Doe', age: 32, role: 'Administrator' },
{ type: 'admin', name: 'Bruce Willis', age: 64, role: 'World saver' },
{ type: 'admin', name: 'Steve', age: 40, role: 'Steve' },
{ type: 'admin', name: 'Will Bruces', age: 30, role: 'Overseer' },
{ type: 'admin', name: 'Superwoman', age: 28, role: 'Customer support' }
];
const users: User[] = [
{ type: 'user', name: 'Max Mustermann', age: 25, occupation: 'Chimney sweep' },
{ type: 'user', name: 'Kate Müller', age: 23, occupation: 'Astronaut' },
{ type: 'user', name: 'Moses', age: 70, occupation: 'Desert guide' },
{ type: 'user', name: 'Superman', age: 28, occupation: 'Ordinary person' },
{ type: 'user', name: 'Inspector Gadget', age: 31, occupation: 'Undercover' }
];
const isAdmin = (person: Person): person is Admin => person.type === 'admin';
const isUser = (person: Person): person is User => person.type === 'user';
const nameDecorators = [
strReverse,
strToLower,
strToUpper,
strRandomize,
strInvertCase
];
function logPerson(person: Person) {
let additionalInformation: string = '';
if (isAdmin(person)) {
additionalInformation = person.role;
}
if (isUser(person)) {
additionalInformation = person.occupation;
}
const randomNameDecorator = nameDecorators[
Math.round(Math.random() * (nameDecorators.length - 1))
];
const name = randomNameDecorator(person.name);
console.log(
` - ${chalk.green(name)}, ${person.age}, ${additionalInformation}`
);
}
([] as Person[])
.concat(users, admins)
.forEach(logPerson);
-
第十一題,我使用了泛型和 declare module
解題,若是你有更優解法,能夠跟我聊聊,自認爲這題沒毛病.上面那道題我說兩個interface
能夠進一步優化,也能夠參考這個思路
type func = <T>(input: T[], comparator: (a: T, b: T) => number) => T | null;
type index<L> = <T>(input: T[], comparator: (a: T, b: T) => number) => L;
declare module 'stats' {
export const getMaxIndex: index<number>;
export const getMaxElement: func;
export const getMinIndex: index<number>;
export const getMinElement: func;
export const getMedianIndex: index<number>;
export const getMedianElement: func;
export const getAverageValue: func;
}
import {
getMaxIndex,
getMaxElement,
getMinIndex,
getMinElement,
getMedianIndex,
getMedianElement,
getAverageValue
} from 'stats';
interface User {
type: 'user';
name: string;
age: number;
occupation: string;
}
interface Admin {
type: 'admin';
name: string;
age: number;
role: string;
}
const admins: Admin[] = [
{ type: 'admin', name: 'Jane Doe', age: 32, role: 'Administrator' },
{ type: 'admin', name: 'Bruce Willis', age: 64, role: 'World saver' },
{ type: 'admin', name: 'Steve', age: 40, role: 'Steve' },
{ type: 'admin', name: 'Will Bruces', age: 30, role: 'Overseer' },
{ type: 'admin', name: 'Superwoman', age: 28, role: 'Customer support' }
];
const users: User[] = [
{ type: 'user', name: 'Max Mustermann', age: 25, occupation: 'Chimney sweep' },
{ type: 'user', name: 'Kate Müller', age: 23, occupation: 'Astronaut' },
{ type: 'user', name: 'Moses', age: 70, occupation: 'Desert guide' },
{ type: 'user', name: 'Superman', age: 28, occupation: 'Ordinary person' },
{ type: 'user', name: 'Inspector Gadget', age: 31, occupation: 'Undercover' }
];
function logUser(user: User | null) {
if (!user) {
console.log(' - none');
return;
}
const pos = users.indexOf(user) + 1;
console.log(` - #${pos} User: ${chalk.green(user.name)}, ${user.age}, ${user.occupation}`);
}
function logAdmin(admin: Admin | null) {
if (!admin) {
console.log(' - none');
return;
}
const pos = admins.indexOf(admin) + 1;
console.log(` - #${pos} Admin: ${chalk.green(admin.name)}, ${admin.age}, ${admin.role}`);
}
const compareUsers = (a: User, b: User) => a.age - b.age;
const compareAdmins = (a: Admin, b: Admin) => a.age - b.age;
const colorizeIndex = (value: number) => chalk.red(String(value + 1));
console.log(chalk.yellow('Youngest user:'));
logUser(getMinElement(users, compareUsers));
console.log(` - was ${colorizeIndex(getMinIndex(users, compareUsers))}th to register`);
console.log();
console.log(chalk.yellow('Median user:'));
logUser(getMedianElement(users, compareUsers));
console.log(` - was ${colorizeIndex(getMedianIndex(users, compareUsers))}th to register`);
console.log();
console.log(chalk.yellow('Oldest user:'));
logUser(getMaxElement(users, compareUsers));
console.log(` - was ${colorizeIndex(getMaxIndex(users, compareUsers))}th to register`);
console.log();
console.log(chalk.yellow('Average user age:'));
console.log(` - ${chalk.red(String(getAverageValue(users, ({age}: User) => age)))} years`);
console.log();
console.log(chalk.yellow('Youngest admin:'));
logAdmin(getMinElement(admins, compareAdmins));
console.log(` - was ${colorizeIndex(getMinIndex(users, compareUsers))}th to register`);
console.log();
console.log(chalk.yellow('Median admin:'));
logAdmin(getMedianElement(admins, compareAdmins));
console.log(` - was ${colorizeIndex(getMedianIndex(users, compareUsers))}th to register`);
console.log();
console.log(chalk.yellow('Oldest admin:'));
logAdmin(getMaxElement(admins, compareAdmins));
console.log(` - was ${colorizeIndex(getMaxIndex(users, compareUsers))}th to register`);
console.log();
console.log(chalk.yellow('Average admin age:'));
console.log(` - ${chalk.red(String(getAverageValue(admins, ({age}: Admin) => age)))} years`);
-
第十二題,考察模塊擴充模式和泛型
import 'date-wizard';
declare module 'date-wizard' {
// Add your module extensions here.
export function pad<T>(name: T): T;
export function dateDetails<T>(name: T): { hours: number };
}
//..
import * as dateWizard from 'date-wizard';
import './module-augmentations/date-wizard';
interface User {
type: 'user';
name: string;
age: number;
occupation: string;
registered: Date;
}
interface Admin {
type: 'admin';
name: string;
age: number;
role: string;
registered: Date;
}
type Person = User | Admin;
const admins: Admin[] = [
{ type: 'admin', name: 'Jane Doe', age: 32, role: 'Administrator', registered: new Date('2016-06-01T16:23:13') },
{ type: 'admin', name: 'Bruce Willis', age: 64, role: 'World saver', registered: new Date('2017-02-11T12:12:11') },
{ type: 'admin', name: 'Steve', age: 40, role: 'Steve', registered: new Date('2018-01-05T11:02:30') },
{ type: 'admin', name: 'Will Bruces', age: 30, role: 'Overseer', registered: new Date('2018-08-12T10:01:24') },
{ type: 'admin', name: 'Superwoman', age: 28, role: 'Customer support', registered: new Date('2019-03-25T07:51:05') }
];
const users: User[] = [
{ type: 'user', name: 'Max Mustermann', age: 25, occupation: 'Chimney sweep', registered: new Date('2016-02-15T09:25:13') },
{ type: 'user', name: 'Kate Müller', age: 23, occupation: 'Astronaut', registered: new Date('2016-03-23T12:47:03') },
{ type: 'user', name: 'Moses', age: 70, occupation: 'Desert guide', registered: new Date('2017-02-19T17:22:56') },
{ type: 'user', name: 'Superman', age: 28, occupation: 'Ordinary person', registered: new Date('2018-02-25T19:44:28') },
{ type: 'user', name: 'Inspector Gadget', age: 31, occupation: 'Undercover', registered: new Date('2019-03-25T09:29:12') }
];
const isAdmin = (person: Person): person is Admin => person.type === 'admin';
const isUser = (person: Person): person is User => person.type === 'user';
function logPerson(person: Person, index: number) {
let additionalInformation: string = '';
if (isAdmin(person)) {
additionalInformation = person.role;
}
if (isUser(person)) {
additionalInformation = person.occupation;
}
let registeredAt = dateWizard(person.registered, '{date}.{month}.{year} {hours}:{minutes}');
let num = `#${dateWizard.pad(index + 1)}`;
console.log(
` - ${num}: ${chalk.green(person.name)}, ${person.age}, ${additionalInformation}, ${registeredAt}`
);
}
console.log(chalk.yellow('All users:'));
([] as Person[])
.concat(users, admins)
.forEach(logPerson);
console.log();
console.log(chalk.yellow('Early birds:'));
([] as Person[])
.concat(users, admins)
.filter((person) => dateWizard.dateDetails(person.registered).hours < 10)
.forEach(logPerson);
-
第十三題,略有難度,考察聯合類型、類型斷言、泛型,因爲數據庫的查詢, find
方法可能傳入的參數是多種多樣的,爲了知足如下的find
使用場景,須要定義Database
這個類
interface User {
_id: number;
name: string;
age: number;
occupation: string;
registered: string;
}
interface Admin {
_id: number;
name: string;
age: number;
role: string;
registered: string;
}
async function testUsersDatabase() {
const usersDatabase = new Database<User>(path.join(__dirname, 'users.txt'), [
'name',
'occupation',
]);
// $eq operator means "===", syntax {fieldName: {$gt: value}}
// see more https://docs.mongodb.com/manual/reference/operator/query/eq/
expect(
(await usersDatabase.find({ occupation: { $eq: 'Magical entity' } })).map(
({ _id }) => _id
)
).to.have.same.members([6, 8]);
expect(
(
await usersDatabase.find({
age: { $eq: 31 },
name: { $eq: 'Inspector Gadget' },
})
)[0]._id
).to.equal(5);
// $gt operator means ">", syntax {fieldName: {$gt: value}}
// see more https://docs.mongodb.com/manual/reference/operator/query/gt/
expect(
(await usersDatabase.find({ age: { $gt: 30 } })).map(({ _id }) => _id)
).to.have.same.members([3, 5, 6, 8]);
// $lt operator means "<", syntax {fieldName: {$lt: value}}
// see more https://docs.mongodb.com/manual/reference/operator/query/lt/
expect(
(await usersDatabase.find({ age: { $lt: 30 } })).map(({ _id }) => _id)
).to.have.same.members([0, 2, 4, 7]);
// $and condition is satisfied when all the nested conditions are satisfied: {$and: [condition1, condition2, ...]}
// see more https://docs.mongodb.com/manual/reference/operator/query/and/
// These examples return the same result:
// usersDatabase.find({age: {$eq: 31}, name: {$eq: 'Inspector Gadget'}});
// usersDatabase.find({$and: [{age: {$eq: 31}}, {name: {$eq: 'Inspector Gadget'}}]});
expect(
(
await usersDatabase.find({
$and: [{ age: { $gt: 30 } }, { age: { $lt: 40 } }],
})
).map(({ _id }) => _id)
).to.have.same.members([5, 6]);
// $or condition is satisfied when at least one nested condition is satisfied: {$or: [condition1, condition2, ...]}
// see more https://docs.mongodb.com/manual/reference/operator/query/or/
expect(
(
await usersDatabase.find({
$or: [{ age: { $gt: 90 } }, { age: { $lt: 30 } }],
})
).map(({ _id }) => _id)
).to.have.same.members([0, 2, 4, 7, 8]);
// $text operator means full text search. For simplicity this means finding words from the full-text search
// fields which are specified in the Database constructor. No stemming or language processing other than
// being case insensitive is not required.
// Syntax {$text: 'Hello World'} - this return all records having both words in its full-text search fields.
// It is also possible that queried words are spread among different full-text search fields.
expect(
(await usersDatabase.find({ $text: 'max' })).map(({ _id }) => _id)
).to.have.same.members([0, 7]);
expect(
(await usersDatabase.find({ $text: 'Hey' })).map(({ _id }) => _id)
).to.have.same.members([]);
// $in operator checks if entry field value is within the specified list of accepted values.
// Syntax {fieldName: {$in: [value1, value2, value3]}}
// Equivalent to {$or: [{fieldName: {$eq: value1}}, {fieldName: {$eq: value2}}, {fieldName: {$eq: value3}}]}
// see more https://docs.mongodb.com/manual/reference/operator/query/in/
expect(
(await usersDatabase.find({ _id: { $in: [0, 1, 2] } })).map(
({ _id }) => _id
)
).to.have.same.members([0, 2]);
expect(
(await usersDatabase.find({ age: { $in: [31, 99] } })).map(({ _id }) => _id)
).to.have.same.members([5, 8]);
}
async function testAdminsDatabase() {
const adminsDatabase = new Database<Admin>(
path.join(__dirname, 'admins.txt'),
['name', 'role']
);
expect(
(await adminsDatabase.find({ role: { $eq: 'Administrator' } })).map(
({ _id }) => _id
)
).to.have.same.members([0, 6]);
expect(
(
await adminsDatabase.find({
age: { $eq: 51 },
name: { $eq: 'Bill Gates' },
})
)[0]._id
).to.equal(7);
expect(
(await adminsDatabase.find({ age: { $gt: 30 } })).map(({ _id }) => _id)
).to.have.same.members([0, 2, 3, 6, 7, 8]);
expect(
(await adminsDatabase.find({ age: { $lt: 30 } })).map(({ _id }) => _id)
).to.have.same.members([5]);
expect(
(
await adminsDatabase.find({
$and: [{ age: { $gt: 30 } }, { age: { $lt: 40 } }],
})
).map(({ _id }) => _id)
).to.have.same.members([0, 8]);
expect(
(
await adminsDatabase.find({
$or: [{ age: { $lt: 30 } }, { age: { $gt: 60 } }],
})
).map(({ _id }) => _id)
).to.have.same.members([2, 5]);
expect(
(await adminsDatabase.find({ $text: 'WILL' })).map(({ _id }) => _id)
).to.have.same.members([4, 6]);
expect(
(await adminsDatabase.find({ $text: 'Administrator' })).map(
({ _id }) => _id
)
).to.have.same.members([0, 6]);
expect(
(await adminsDatabase.find({ $text: 'Br' })).map(({ _id }) => _id)
).to.have.same.members([]);
expect(
(await adminsDatabase.find({ _id: { $in: [0, 1, 2, 3] } })).map(
({ _id }) => _id
)
).to.have.same.members([0, 2, 3]);
expect(
(await adminsDatabase.find({ age: { $in: [30, 28] } })).map(
({ _id }) => _id
)
).to.have.same.members([4, 5]);
}
Promise.all([testUsersDatabase(), testAdminsDatabase()]).then(
() => console.log('All tests have succeeded, congratulations!'),
(e) => console.error(e.stack)
);
//Database
import * as fs from 'fs';
export type FieldOp =
| { $eq: string | number }
| { $gt: string | number }
| { $lt: string | number }
| { $in: (string | number)[] };
export type Query =
| { $and: Query[] }
| { $or: Query[] }
| { $text: string }
| ({ [field: string]: FieldOp } & {
$and?: never;
$or?: never;
$text?: never;
});
function matchOp(op: FieldOp, v: any) {
if ('$eq' in op) {
return v === op['$eq'];
} else if ('$gt' in op) {
return v > op['$gt'];
} else if ('$lt' in op) {
return v < op['$lt'];
} else if ('$in' in op) {
return op['$in'].includes(v);
}
throw new Error(`Unrecognized op: ${op}`);
}
function matches(q: Query, r: unknown): boolean {
if ('$and' in q) {
return q.$and!.every((subq) => matches(subq, r));
} else if ('$or' in q) {
return q.$or!.some((subq) => matches(subq, r));
} else if ('$text' in q) {
const words = q.$text!.toLowerCase().split(' ');
return words.every((w) => (r as any).$index[w]);
}
return Object.entries(q).every(([k, v]) => matchOp(v, (r as any)[k]));
}
export class Database<T> {
protected filename: string;
protected fullTextSearchFieldNames: string[];
protected records: T[];
constructor(filename: string, fullTextSearchFieldNames: string[]) {
this.filename = filename;
this.filename = filename;
this.fullTextSearchFieldNames = fullTextSearchFieldNames;
this.fullTextSearchFieldNames = fullTextSearchFieldNames;
const text = fs.readFileSync(filename, 'utf8');
const lines = text.split('\n');
this.records = lines
.filter((line) => line.startsWith('E'))
.map((line) => JSON.parse(line.slice(1)))
.map((obj) => {
obj.$index = {};
for (const f of fullTextSearchFieldNames) {
const text = obj[f];
for (const word of text.split(' ')) {
obj.$index[word.toLowerCase()] = true;
}
}
return obj;
});
}
async find(query: Query): Promise<T[]> {
return this.records.filter((r) => matches(query, r));
}
}
寫在最後
-
我是 Peter醬
,前端架構師 擅長跨平臺和極限場景性能優化 不按期會給大家推送高質量原創文章,公衆號回覆:加羣
便可加入羣聊 -
我把個人往期文章、源碼都放在一個倉庫了,手寫 vue React webpack weboscket redis 靜態資源服務器
(實現http
緩存)redux promise react-redux 微前端框架 ssr
等,進階前端專家不可或缺.
-
【 前端巔峯
】即將迎來技術團隊入駐,爲打造更多前端技術專而奮鬥.記得點個關注
、在看
。
本文分享自微信公衆號 - 前端巔峯(Java-Script-)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。前端