如何为前端项目一键自动添加eslint和prettier的支持

简介: 本文以vite脚手架创建的项目为基础进行研究的,如果是其他脚手架创建的项目,那么就要自己去修改处理,但是原理是一样的。

image.png


前言


我之前好多次都是一步一步的安装eslint和prettier及相关依赖,一个配置文件一个配置文件的粘贴复制,并修改其中的相关配置。而且可能会在每个项目中都要去处理,如果项目工程规划化以后,eslint和prettier确实是项目少不了的配置。不知道你有没有像我一样操作过呢?


那么有没有一种更简单的方式去处理呢?答案是我终于遇到了。通过若川大佬的源码共读活动发现了,真的是太棒了。


本文以vite脚手架创建的项目为基础进行研究的,如果是其他脚手架创建的项目,那么就要自己去修改处理,但是原理是一样的。


那么接下来,我就要来一探究竟,先看看如何使用,然后查阅一下它的源码,看看它到底是如何实现的呢?


1、vite创建项目


  • 创建项目


yarn create vite


一顿操作以后项目就创建完毕了


image.png


  • 2、安装依赖


yarn


  • 3、运行项目


yarn dev


  • 4、运行初始化eslint和prettier命令


yarn create vite-pretty-lint


先来看没有执行命令前的文件目录


image.png


再来看执行完命令后的文件目录


image.png


可以发现文件目录中增加了eslint和prettier的相关配置,package.json中增加了相关的依赖、以及vite.config.xx文件也增加了相关配置,具体的文件变更可以查看github.com/lxchuan12/v…


一个命令干了这么多事情,真的太优秀了。接下来我们就去看看这如此优秀的源代码吧


2、整个过程的示意图


通过大致的查看源代码,简单总结出来的代码执行过程示意图,仅供参考


image.png


3、源码调试过程


3.1、找到调试代码的位置


通过package.json中的bin节点可以发现,yarn create vite-pretty-lint最终执行的便是lib/main.js中的代码


"bin": {
    "create-vite-pretty-lint": "lib/main.js"
  },


3.2、 开始调试的命令


因为我们现在只是要执行lib/main.js这个入口文件,通过package.jsonscripts 也没有发现执行命令,所以现在我们可以直接通过node来运行代码


node lib/main.js


调试成功的结果如下图所示


image.png


3.3、 查看头部引入的模块


  • chalk终端多色彩输出


npm i chalk
import chalk from 'chalk'
const log = console.log
// 字体背景颜色设置
log(chalk.bgGreen('chalk打印设置') )
// 字体颜色设置
log(chalk.blue('Hello') + ' World' + chalk.red('!'))
// 自定义颜色
const custom = chalk.hex('#F03A17')
const bgCustom = chalk.bgHex('#FFFFFF')
log(custom('customer'))
log(bgCustom('bgCustom'))


执行效果如下图所示


image.png


  • gradient 文字颜色渐变


// 安装
npm i gradient-string
// 引入
import gradient  from 'gradient-string'
// 使用
console.log(gradient('cyan', 'pink')('你好啊赛利亚欢迎来到编码世界'));
console.log(gradient('cyan', 'pink')('你好啊赛利亚欢迎来到编码世界'));
console.log(gradient('cyan', 'pink')('你好啊赛利亚欢迎来到编码世界'));
console.log(gradient('cyan', 'pink')('你好啊赛利亚欢迎来到编码世界'));
console.log(gradient('cyan', 'pink')('你好啊赛利亚欢迎来到编码世界'));


执行效果如下图所示


image.png


  • child_process node.js中的子进程。


在node.js中,只有一个线程执行所有的操作,如果某个操作需要大量消耗CPU资源的话,后续的操作就需要等待。后来node.js就提供了一个child_process模块,通过它可以开启多个子进程,在多个子进程之间可以共享内存空间,可以通过子进程的互相通信来实现信息的交换。


import { exec } from 'child_process';
exec('ls',(error, stdout,stderr)=> {
    if(error) {
        console.log(error)
        return;
    }
    console.log('stdout: ' + stdout)
    console.log('执行其他操作')
})


执行效果如下图所示


image.png


  • fs fs用来操作文件的模块


import fs from 'fs'
// 同步的读取方法,用来读取指定文件中的内容
fs.readFileSync() 
// 同步的写入方法,用来向指定文件中写内容
fs.writeFileSync() 


  • path路径分类


import path from 'path';
// 拼接路径
console.log(path.join('src', 'task.js'));  // src/task.js


  • nanospinner命令行中的加载动画


// 安装
npm i nanospinner
// 引入模块
import { createSpinner } from 'nanospinner';
const spinner = createSpinner('Run test').start()
setTimeout(() => {
  spinner.success()
}, 1000)


执行效果如下图所示(Run test在加载的一个效果)


  • enquirer (utils.js文件)


交互式询问CLI 简单说就是交互式询问用户输入


npm i enquirer 
import enquirer from 'enquirer' 
let tempArray = ['major(1.0.0)','minor(0.1.0)', 'patch(0.0.4)', "customer" ]
const { release } = await enquirer.prompt({
    type: 'select',
    name: 'release',
    message: 'Select release type',
    choices: tempArray
})
if(release === 'customer') {
    console.log(release, 'customer')
} else {
    const targetVersion = release.match(/\((.*)\)/)[1]
    console.log(targetVersion, 'targetVersion')
}


执行效果如下图所示:先出来一个下拉选择,选择完后根据if判断进行输出


3.4、 调试具体代码


3.4.1、 main.js中的入口


async function run() {
    // 所有的逻辑代码
}
run().catch((e) => {
  console.error(e);
});


通过run函数封装异步方法,这样最外面调用run函数时可以通过异步方法的catch捕获错误异常。


看一个小例子


const runTest = async () => {
    console.log('Running test')
    throw new Error('run test报错了')
}
runTest().catch(err => {
    console.log('Error: ' + err)
})


执行后打印顺序如下


Running test
Error: Error: run test报错了


可以发现catch中截获了异常


接下来开始进入run函数了


3.4.2、 打印色彩字体


// 这个看上面的引入模块解析即可
console.log(
    chalk.bold(
      gradient.morning('\n🚀 Welcome to Eslint & Prettier Setup for Vite!\n')
    )
);


3.4.3、 交互式命令行


export function getOptions() {
  const OPTIONS = [];
  fs.readdirSync(path.join(__dirname, 'templates')).forEach((template) => {
    const { name } = path.parse(path.join(__dirname, 'templates', template));
    OPTIONS.push(name);
  });
  return OPTIONS;
}
export function askForProjectType() {
  return enquirer.prompt([
    {
      type: 'select',
      name: 'projectType',
      message: 'What type of project do you have?',
      choices: getOptions(),
    },
    {
      type: 'select',
      name: 'packageManager',
      message: 'What package manager do you use?',
      choices: ['npm', 'yarn'],
    },
  ]);
}
  try {
    const answers = await askForProjectType();
    projectType = answers.projectType;
    packageManager = answers.packageManager;
  } catch (error) {
    console.log(chalk.blue('\n👋 Goodbye!'));
    return;
  }


getOptions 函数根据fs.readdirSync读取项目工程template文件夹下的所有文件,并通过path.parse转换对象,来获取文件名称name


askForProjectType函数通过enquirer.prompt返回两个交互式命令行,供用户进行选择projectType选择项目类型: 【react-ts】 【react】【vue-ts】 【vue】packageManager选择项目包管理方式:【npm】 【yarn】


3.4.4、根据交互命令行返回结果进行匹配模板


假如我们上面选择的是[vue-ts]


const { packages, eslintOverrides } = await import(
    `./templates/${projectType}.js`
);


/template/vue-ts.js模板中的代码(其中代码较多但一看就明白我就不贴了),就是export导出了两个固定的模板变量数组,packages则相当于要引入的npm模块列表,eslintOverrides这算是.eslintrc.json初始化模板。


3.4.5、拼接变量数组


const packageList = [...commonPackages, ...packages];
const eslintConfigOverrides = [...eslintConfig.overrides, ...eslintOverrides];
const eslint = { ...eslintConfig, overrides: eslintConfigOverrides };


commonPackagesshared.js中预定义的公共的npm 模块eslint则是通过公共npm模块中的eslintConfig和上面选择的template/xxxx.js中的进行拼接组成。


3.4.6、 生成安装依赖包的命令


const commandMap = {
    npm: `npm install --save-dev ${packageList.join(' ')}`,
    yarn: `yarn add --dev ${packageList.join(' ')}`,
};


packageList数组通过join转换为字符串,通过命令将所有拼接npm模块一起安装


image.png


3.4.7、 读取项目的vite配置文件


const projectDirectory = process.cwd();
  const viteJs = path.join(projectDirectory, 'vite.config.js');
  const viteTs = path.join(projectDirectory, 'vite.config.ts');
  const viteMap = {
    vue: viteJs,
    react: viteJs,
    'vue-ts': viteTs,
    'react-ts': viteTs,
  };
  const viteFile = viteMap[projectType];
  const viteConfig = viteEslint(fs.readFileSync(viteFile, 'utf8'));
  const installCommand = commandMap[packageManager];
  if (!installCommand) {
    console.log(chalk.red('\n✖ Sorry, we only support npm and yarn!'));
    return;
  }


根据选择的项目类型,来拼接vite.config的路径,并读取项目中的vite.config配置文件


上面用到了一个函数viteEslint,这个具体的实现可以去看shared.js中,主要就是读取文件内容后,传入的参数code,就是vite.config.ts中的所有字符


image.png


通过babel的parseSync转换为ast。ast对象如下图所示


image.png


对ast数据进行了一系列的处理后,再通过babeltransformFromAstSync将ast转换为代码字符串。


对于babel处理这一块我也不太了解,有时间我得去加一下餐,具体的可以参考 juejin.cn/post/684490…


3.4.8 执行命令、执行完将eslint和prettier配置重写


const spinner = createSpinner('Installing packages...').start();
  exec(`${commandMap[packageManager]}`, { cwd: projectDirectory }, (error) => {
    if (error) {
      spinner.error({
        text: chalk.bold.red('Failed to install packages!'),
        mark: '✖',
      });
      console.error(error);
      return;
    }
    const eslintFile = path.join(projectDirectory, '.eslintrc.json');
    const prettierFile = path.join(projectDirectory, '.prettierrc.json');
    const eslintIgnoreFile = path.join(projectDirectory, '.eslintignore');
    fs.writeFileSync(eslintFile, JSON.stringify(eslint, null, 2));
    fs.writeFileSync(prettierFile, JSON.stringify(prettierConfig, null, 2));
    fs.writeFileSync(eslintIgnoreFile, eslintIgnore.join('\n'));
    fs.writeFileSync(viteFile, viteConfig);
    spinner.success({ text: chalk.bold.green('All done! 🎉'), mark: '✔' });
    console.log(
      chalk.bold.cyan('\n🔥 Reload your editor to activate the settings!')
    );
  });


首先通过createSpinner来创建一个命令行中的加载,然后通过child_process中的exec来执行[3.4.6]中生成的命令,去安装依赖并进行等待。


如果命令执行成功,则通过fs.writeFileSync将生成的数据写入到三个文件当中.eslintrc.json.prettierrc.json.eslintignorevite.config.xx


4、npm init、npx


印象里面大家可能对它的记忆可能都停留在,npm init之后是快速的初始化package.json,并通过交互式的命令行让我们输入需要的字段值,当然如果想直接使用默认值,也可以使用npm init -y


create-app-react创建项目命令,官网链接可以直接查看 create-react-app.dev/docs/gettin…


//官网的三种命令
npx create-react-app my-app
npm init react-app my-app
yarn create react-app my-app
//我又发现npm create也是可以的
npm create react-app my-app


上述这些命令最终效果都是可以执行创建项目的


同样的vite创建项目的命令


//官网的命令
npm create vite@latest
yarn create vite
pnpm create vite
// 指定具体模板的
// npm 6.x 
npm create vite@latest my-vue-app --template vue 
//npm 7+, extra double-dash is needed: 
npm create vite@latest my-vue-app -- --template vue
yarn create vite my-vue-app --template vue
pnpm create vite my-vue-app --template vue


可以发现vite官网没有使用npx命令,不过我在我自己电脑上尝试了另外几个命令确实也是可以的


npx create-vite my-app
npm init vite my-app


image.png


通过上面的对比可以一个小问题,yarn create去官网查了是存在这个指令的,官网地址可看 classic.yarnpkg.com/en/docs/cli…


而对于npm create这个命令在npm官网是看不到的,但是在一篇博客中发现了更新日志


image.png


意思就是说npm create xxxnpm init xxx 以及yarn create xxx效果是一致的。那么我们来本文的命令行


// 我们是通过npm安装的,并且包名里是包含create的
npm i create-vite-pretty-lint
// 那么以下几种方式都可以使用的
npm init vite-pretty-lint
npm create vite-pretty-lint
yarn create vite-pretty-lint
npx create-vite-pretty-lint


再来看一下npx


假如我们只在项目中安装了vite,那么node_modules.bin文件夹下是会存在vite指令的


image.png


如果我们想在该项目下执行该命令第一种方式便是


image.png


第二种方式就是直接在package.json的scripts属性下


image.png


关于npx的详细说明可以看一下阮一峰大佬的精彩分享 www.ruanyifeng.com/blog/2019/0…


5、总结


  • npm init xxx的妙用,以及对npx的了解,感觉对package.json的每一个属性,可以专门去学习一下


  • 对于自动添加eslint和prettier配置的原理分析


  • .eslintrc.json、.eslintignore、.prettierrc.json算是直接新增文件,处理相对简单一些


  • 最重要的学习点:对vite.config文件在原有基础上的修改,这里就涉及到了AST抽象语法树


6、加餐 V8下的AST抽象语法树


有兴趣的话可以看看我前几天刚刚总结的关于V8引擎是如何运行JavaScript代码的,其中就涉及到关于AST的部分juejin.cn/post/710941…


接下来有时间我会简单的把AST详细的学习一下,查了很多资料发现AST还是非常重要的,无论是babel、webpack、vite、vue、react、typescript等都使用到了AST。

目录
相关文章
|
8月前
|
JSON 自然语言处理 前端开发
【01】对APP进行语言包功能开发-APP自动识别地区ip后分配对应的语言功能复杂吗?-成熟app项目语言包功能定制开发-前端以uniapp-基于vue.js后端以laravel基于php为例项目实战-优雅草卓伊凡
【01】对APP进行语言包功能开发-APP自动识别地区ip后分配对应的语言功能复杂吗?-成熟app项目语言包功能定制开发-前端以uniapp-基于vue.js后端以laravel基于php为例项目实战-优雅草卓伊凡
461 72
【01】对APP进行语言包功能开发-APP自动识别地区ip后分配对应的语言功能复杂吗?-成熟app项目语言包功能定制开发-前端以uniapp-基于vue.js后端以laravel基于php为例项目实战-优雅草卓伊凡
|
10月前
|
Dart 前端开发
【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
368 75
【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
|
9月前
|
前端开发 Java Shell
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
627 20
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
|
9月前
|
安全 前端开发 开发工具
【01】鸿蒙实战应用开发-华为鸿蒙纯血操作系统Harmony OS NEXT-项目开发实战-优雅草卓伊凡拟开发一个一站式家政服务平台-前期筹备-暂定取名斑马家政软件系统-本项目前端开源-服务端采用优雅草蜻蜓Z系统-搭配ruoyi框架admin后台-全过程实战项目分享-从零开发到上线
【01】鸿蒙实战应用开发-华为鸿蒙纯血操作系统Harmony OS NEXT-项目开发实战-优雅草卓伊凡拟开发一个一站式家政服务平台-前期筹备-暂定取名斑马家政软件系统-本项目前端开源-服务端采用优雅草蜻蜓Z系统-搭配ruoyi框架admin后台-全过程实战项目分享-从零开发到上线
472 5
【01】鸿蒙实战应用开发-华为鸿蒙纯血操作系统Harmony OS NEXT-项目开发实战-优雅草卓伊凡拟开发一个一站式家政服务平台-前期筹备-暂定取名斑马家政软件系统-本项目前端开源-服务端采用优雅草蜻蜓Z系统-搭配ruoyi框架admin后台-全过程实战项目分享-从零开发到上线
|
9月前
|
JSON 前端开发 API
以项目登录接口为例-大前端之开发postman请求接口带token的请求测试-前端开发必学之一-如果要学会联调接口而不是纯写静态前端页面-这个是必学-本文以优雅草蜻蜓Q系统API为实践来演示我们如何带token请求接口-优雅草卓伊凡
以项目登录接口为例-大前端之开发postman请求接口带token的请求测试-前端开发必学之一-如果要学会联调接口而不是纯写静态前端页面-这个是必学-本文以优雅草蜻蜓Q系统API为实践来演示我们如何带token请求接口-优雅草卓伊凡
498 5
以项目登录接口为例-大前端之开发postman请求接口带token的请求测试-前端开发必学之一-如果要学会联调接口而不是纯写静态前端页面-这个是必学-本文以优雅草蜻蜓Q系统API为实践来演示我们如何带token请求接口-优雅草卓伊凡
|
10月前
|
Dart 前端开发 容器
【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
346 18
【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
|
10月前
|
前端开发 Java 开发工具
【03】完整flutter的APP打包流程-以apk设置图标-包名-签名-APP名-打包流程为例—-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈 章节内容【03】
【03】完整flutter的APP打包流程-以apk设置图标-包名-签名-APP名-打包流程为例—-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈 章节内容【03】
875 18
【03】完整flutter的APP打包流程-以apk设置图标-包名-签名-APP名-打包流程为例—-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈 章节内容【03】
|
9月前
|
Dart 前端开发 Android开发
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
299 4
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
|
8月前
|
人工智能 JavaScript 前端开发
Vue 性能革命:揭秘前端优化的终极技巧;Vue优化技巧,解决Vue项目卡顿问题
Vue在处理少量数据和有限dom的情况下技术已经非常成熟了,但现在随着AI时代的到来,海量数据场景会越来越多,Vue优化技巧也是必备技能。 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
10月前
|
缓存 前端开发 Android开发
【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
488 12
【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程

热门文章

最新文章

  • 1
    前端如何存储数据:Cookie、LocalStorage 与 SessionStorage 全面解析
  • 2
    前端工程化演进之路:从手工作坊到AI驱动的智能化开发
  • 3
    Vue 3 + TypeScript 现代前端开发最佳实践(2025版指南)
  • 4
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(五):背景属性;float浮动和position定位;详细分析相对、绝对、固定三种定位方式;使用浮动并清除浮动副作用
  • 5
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(六):全方面分析css的Flex布局,从纵、横两个坐标开始进行居中、两端等元素分布模式;刨析元素间隔、排序模式等
  • 6
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(一):CSS发展史;CSS样式表的引入;CSS选择器使用,附带案例介绍
  • 7
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(八):学习transition过渡属性;本文学习property模拟、duration过渡时间指定、delay时间延迟 等多个参数
  • 8
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(九):强势分析Animation动画各类参数;从播放时间、播放方式、播放次数、播放方向、播放状态等多个方面,完全了解CSS3 Animation
  • 9
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(四):元素盒子模型;详细分析边框属性、盒子外边距
  • 10
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(二):CSS伪类:UI伪类、结构化伪类;通过伪类获得子元素的第n个元素;创建一个伪元素展示在页面中;获得最后一个元素;处理聚焦元素的样式
  • 下一篇
    oss云网关配置