《前端规范》


2019-02-12 上次更新时间:8/4/2020, 8:24:44 PM 0 前端

# 1. 前言

# 主要目的

1.提高代码的规范性可维护性。通过保持代码风格和传统的一致性,我们可以减少遗留系统维护的负担,并降低未来系统崩溃的风险,减少团队成员切换项目的熟悉时间。

2.最佳实践。通过遵照最佳实践,我们能确保优化的页面加载、性能以及可维护的代码。

# 核心思想

1.表现、内容和行为的分离 2.标记应该是结构良好、语义正确 以及 普遍合法 。 3.Javascript应该起到渐进式增强用户体验的作用 。

# 2. 命名规则

  • 项目命名
  • 目录命名
  • Vue文件目录命名
  • Js文件命名
  • Css、Scss文件命名
  • 图片命名

整体项目命名示例请查看示例文件夹下的项目目录结构

# 项目命名

名称全部采用小写,尽量不要缩写,以中划线分隔。

命名格式:<业务类型>-<项目类型>-<端>

(1) 业务类型。如:

  • 商家:merchant
  • 创新业务:innovation
  • 预订业务:booking

(2) 项目类型。如:

  • 营销通:market
  • 门店通:hotel
  • 运营平台:operating

(3) 端。如:

  • 移动端:mobile
  • pc端:pc
  • Android客户端:android
  • Ios: ios

例:

营销通移动端:merchant-market-mobile 营销通pc端:merchant-market-pc

门店通移动端:merchant-hotel-pc 门店通pc端:merchant-hotel-h5

# 目录命名

名称全部采用小写,尽量不要缩写。有复数结构时,要采用复数命名法。

如:components、services、directives、filters、utils、styles。

# Vue文件目录命名

参考官方给出的:风格指南

1、文件夹命名。

1.1、 components 文件夹下的子文件夹统一使用PascalBase风格

公用的业务组件放在最外层的components文件夹下,如各业务组件需要抽离该业务下的组件,则放在各页面下的./components文件夹下。

每个components文件夹下最多只有一层文件夹,且文件夹名称为组件名称,文件夹下必须有index.vueindex.js

原因:公用组件使用PascalBase风格,除了是为了在引用组件时候能够风格统一,还能将引用的组件和.vue组件内部的使用驼峰法声明的变量加以区别。export { default as Sidebar } from './Sidebar'

1.2、除components文件夹下的文件名,其他文件目录名称统一使用kebab-case格式。如活动中心模块(activity-center)、活动报名模块(activity-apply)。

2、组件文件命名(.vue文件):组件名应始终是多个单词的,除根组件APP.vue 和index.vue外。单文件组件的文件名始终是PascalBase风格。如:SideBar.vue、NavBar.vue。

2.1 基础组件(应用特定样式和约定的基础组件,即仅用于展示的无逻辑无状态的组件)命名:以Base/APP作为特定的前缀

如:

BaseButton.vue

BaseTable.vue

BaseIcon.vue

2.2 紧密耦合的组件名,应以父组件名作为前缀名。

如:

SearchSidebar.vue

SearchSidebarNav.vue

# Js文件命名

单个单词命名的js文件,如index.js、date.js。

多个单词命名的js文件统一使用camelCase驼峰命名法,如:dealErrorMsg.js、getToken.js。

# Css、Scss文件命名

名称全部采用小写,尽量使用一个单词说明,如有需要使用多个单词,则以kebab-case格式。

公用css文件命名:

css reset文件: normalize.css/reset.css 全局公用的样式文件:common.css/app.css

注意:编写样式时多看看公用样式文件,看看是否已存在,避免样式冗余。如果有抽取了公用样式,记得及时跟其他小伙伴沟通。

组件css文件

当css样式过多,需要新建css文件时,应考虑新建文件夹,将.vue文件和css文件放在同一个文件夹内。此时可以使用index.vue和index.css文件名。

# 图片命名

名称全部采用小写,以中划线分隔,以图片类型作为前缀。

如: img-not-found.png icon-search.png

# 3. Git 使用规范

# 分支规范

分支管理参考git-flow(git-flow介绍)的工作流程,但不使用git-flow。所有的分支的操作都需要手工完成。

1.1、git-flow流程图: https://images.cnblogs.com/cnblogs_com/cnblogsfans/771108/o_git-flow-nvie.png

1.2、分支说明:

featrue/XXX: 功能开发分支,继承自master分支,根据需求或者线上bug创建分支,开发、测试阶段所有改动只在该类型分支上修改;

dev: 开发合并分支,合并来自feature/*的分支代码,注意不要在此分支上做修改提交!!!只做合并操作!!!,并在此分支打包提交测试;

master:发布分支,feature/*测试通过的分支,合并到该分支,注意不要在此分支上做修改提交!!!只做合并操作!!!,并在此分支打包发布。

hotfix/XXX: 热修复分支,继承自master分支,仅用于修复master分支上的问题。完成后合并到各分支。

bugfix/XXX: bug修复分支,继承自master分支,仅用于修复master分支上的问题,完成后合并到各分支。

注意:当合并有冲突时,要解决冲突后再提交,避免造成页面奔溃。不熟悉命令合并的同学可以使用编辑器的merge功能,或者可以使用工具,如:sourceTree

# commit 规范

一般来说,commit message 应该清晰明了,说明本次提交的目的。我们参考目前比较流行的Angular规范

commit 格式<type>: <subject>(注意冒号后面有空格)。

(1)type: 用于说明 commit 的类别,无特殊情况下只使用下面7个标识。

  • feat:新功能(feature)
  • fix:修补bug
  • docs:文档(documentation)
  • style: 格式(不影响代码运行的变动)
  • refactor:重构(即不是新增功能,也不是修改bug的代码变动)
  • test:增加测试
  • chore:构建过程或辅助工具的变动

(2)subject: commit 目的的简短描述,不超过50个字符。

例:

git commit -m "feat: 新增酒店刷新页面"
git commit -m "fix: 活动中心基本信息页活动ID不可选"
git commit -m "doc: readme文件添加项目打包说明"
1
2
3

注意:如果是有多种形式的修改,以最主要的修改选择type,其他修改可在subject中说明。

# 版本发布规范

已经测试完毕的功能分支合并到master,按版本号打tag。

版本号组成::v<主版本号>.<子版本号>.<修订号>。

主版本号前加v标识表示version。如初始版本可为v1.0.0v0.1.0

管理策略:

  • 当项目在进行了局部修改或 bug 修正时,主版本号和子版本号都不变,修正版本号加 1。如v0.1.1
  • 当项目在原有的基础上增加了部分功能时,主版本号不变,子版本号加 1,修正版本号复位为 0。如: v0.2.0;
  • 当项目在进行了重大修改或局部修正累积较多,而导致项目整体发生全局变化时,主版本号加 1。如:v1.0.0;

版本说明:

每一次版本的发布都需要对该次版本上线进行说明,说明信息参考commit规范:

例:

git tag -a v0.1.0 -m "feat: 初始版本上线,包括首页、列表页、详情页等" master
1

建议:可在项目中添加版本说明文档,可方便查看生产环境当前版本。

参考文档:

git官方文档 git教程 百度百科:软件版本号

# 4. JavaScript 规范

勤写注释,再语义化的命名不如一个中文注释

使用目前比较流行的Airbnb语法规范,并搭配Eslint作为语法检查,详情查看以下有关文档。

说明文档: Airbnb JavaScript Style Guide Airbnb JavaScript Style Guide中文翻译 eslint规则说明

提示:各种编辑器都有对应的Eslint插件,可以安装有关插件进行语法检查,如vscode支持文件保存时按照项目下的.eslintrc.js里定义的规则自动格式化,这样使得开发者在开发时并不需要过多的关注是否符合语法。

vscode配置eslint插件:vscode保存代码,自动按照eslint规范格式化代码设置

# 命名

1.变量

命名方式:采用小驼峰式命名法。 命名规范:前缀应当是名词。(函数的名字前缀为动词,以此区分变量和函数) 命名建议:尽量在变量名字中体现所属类型,如:length、count等表示数字类型;而包含name、title表示为字符串类型。

如:

// Good
let maxCount = 10;
let tableTitle = 'LoginTable';

// Bad
let setCount = 10;
let getTitle = 'LoginTable';
1
2
3
4
5
6
7

2.常量

命名方式:必须采用全大写的命名,且单词以_分割,常量通常用于ajax请求url,和一些不会改变的数据。 命名规范:使用大写字母和下划线来组合命名,下划线用以分割单词。

如:

const MAX_COUNT = 10;
const URL = 'http://www.foreverz.com';
1
2

3.函数

命名方法:小驼峰式命名法。 命名规范:前缀应当为动词。 命名建议:可使用常见动词约定

动词 含义 返回
can 判断是否可执行某个动作(权限) 函数返回一个布尔值。true:可执行;false:不可执行
has 判断是否含有某个值 函数返回一个布尔值。true:含有此值;false:不含有此值
is 判断是否为某个值 函数返回一个布尔值。true:为某个值;false:不为某个值
get 获取某个值 函数返回一个非布尔值
set 设置某个值 无返回值、返回是否设置成功或者返回链式对象
load 加载某些数据 无返回值或者返回是否加载完成的结果

如:

//是否仅可读
function isReadonly() {
	return true;
}
// 获取token
function getToken() {
	return 'token';
}
1
2
3
4
5
6
7
8

4.类和构造函数

命名方法:大驼峰式命名法,首字母大写。 命名规范:前缀为名称

4.1 类的成员

  • 公共属性和方法:跟变量和函数的命名一样。
  • 私有属性和方法:前缀为_(下划线),后面跟公共属性和方法一样的命名方式。

如:

class Person {
  // 私有属性
  private _name: string;
  constructor() { }
  // 公共方法
  getName() {
    return this._name;
  }
  // 静态方法
  static _setName(name) {
    this._name = name;
  }
}
const person = new Person();
person.setName('mervyn');
person.getName(); // ->mervyn
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 语法

1.尽量不要混用语法

一般来说,项目开发使用规范应该统一。当使用了Es6规范时,如箭头函数,那么后续编写新函数的时候也应该使用箭头函数的方式。

注意点:

不好的示例:

let innId = '123';
var innName = '7天酒店';

const setNamet = (name) => {
	return name;
}

function getName() {
	return this.innName;
}

const foo = function () {
    // ...
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

2.总是使用带类型判断的比较判断

总是使用===精确的比较操作符,避免在判断的过程中,由 JavaScript 的强制类型转换所造成的困扰。

3.使用更简洁的实现方法

如:

// Bad
if (query.actName) {
	this.map.actName = query.actName;
}
if (query.actStatus) {
	this.map.actStatus = query.actStatus;
}
if (query.bookTime) {
	this.map.bookTime = query.bookTime;
}
if (query.checkInTime) {
	this.map.checkInTime = query.checkInTime;
}
if (query.quotaCrl) {
	this.map.quotaCrl = query.quotaCrl;
}
if (query.rateCrl) {
	this.map.rateCrl = query.rateCrl;
}
this.map.limit = parseInt(query.size || 10);
this.map.page = parseInt(query.page || 1);

// Good
const query = this.$route.query;
Object.keys(this.map).forEach(key => {
	if (this.map.hasOwnProperty(key) && query[key]) {
		if (key === 'bookType') {
			this.map[key] = parseInt(query[key]);
		} else {
			this.map[key] = query[key];
		}
	}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

JavaScript 指南 ECMAScript 6 入门

# 书写规范

1.【缩进】:使用 4 个空格做为一个缩进层级。 2.【空格】 3.以eslint配置的rules为准。。。

# 5. vue 编写规范

参考官方给出的:风格指南

示例请查看示例文件夹下的vue文件编写示例

# prop定义

  • 在声明 prop 的时候,其命名应该始终使用 camelCase,而在模板和 JSX 中应该始终使用 kebab-case。
  • prop定义尽量详细,至少要指定其类型
props: {
  greetingText: {
		required: true,
		type: Boolean,
		default: false
  }
}

// 组件的引用统一使用 kebab-case 格式
<welcome-message greeting-text="hi"/>
1
2
3
4
5
6
7
8
9
10

# 为v-for 设置键值

原因:以便维护内部组件及其子树的状态

<ul>
  <li
    v-for="todo in todos"
    :key="todo.id"
  >
    {{ todo.text }}
  </li>
</ul>
1
2
3
4
5
6
7
8

# 避免v-if 和v-for 用在一起

v-for比v-if有更高的优先级,所以每一次循环都会进行一次if判断,耗费性能。

# 多个特性的元素应该分多行撰写,每个特性一行。

<img
  src="https://vuejs.org/images/logo.png"
  alt="Vue Logo">
1
2
3

# 指令缩写 (用 : 表示 v-bind: 和用 @ 表示 v-on:) 应该要么都用要么都不用。

<input :value="newTodoText" :placeholder="newTodoInstructions"
>
1
2

# 元素特性顺序

1.定义 (提供组件的选项)

  • is

2.列表渲染 (创建多个变化的相同元素)

  • v-for

3.条件渲染 (元素是否渲染/显示)

  • v-if
  • v-else-if
  • v-else
  • v-show
  • v-cloak

4.渲染方式 (改变元素的渲染方式)

  • v-pre
  • v-once

5.全局感知 (需要超越组件的知识)

  • id

6.唯一的特性 (需要唯一值的特性)

  • ref
  • key
  • slot

7.双向绑定 (把绑定和事件结合起来)

  • v-model

8.其它特性 (所有普通的绑定或未绑定的特性)

9.事件 (组件事件监听器)

  • v-on

10.内容 (覆写元素的内容)

  • v-html
  • v-text

# 组件/实例的选项顺序

这是我们推荐的组件选项默认顺序。它们被划分为几大类,所以你也能知道从插件里添加的新属性应该放到哪里。

1.副作用 (触发组件外的影响)

  • el

2.全局感知 (要求组件以外的知识)

  • name
  • parent

3.组件类型 (更改组件的类型)

  • functional

4.模板修改器 (改变模板的编译方式)

  • delimiters
  • comments

5.模板依赖 (模板内使用的资源)

  • components
  • directives
  • filters

6.组合 (向选项里合并属性)

  • extends
  • mixins

7.接口 (组件的接口)

  • inheritAttrs
  • model
  • props/propsData

8.本地状态 (本地的响应式属性)

  • data
  • computed

9.事件 (通过响应式事件触发的回调)

  • watch 生命周期钩子 (按照它们被调用的顺序)
  • beforeCreate
  • created
  • beforeMount
  • mounted
  • beforeUpdate
  • updated
  • activated
  • deactivated
  • beforeDestroy
  • destroyed

10.非响应式的属性 (不依赖响应系统的实例属性)

  • methods

11.渲染 (组件输出的声明式描述)

  • template/render
  • renderError

# 组件样式

  • 为组件样式设置作用域
  • 避免在scoped中出现元素选择器,这样会导致性能变慢。(因为vue为了给样式设置作用域,会给元素添加一个唯一属性,如data-v-1234。当修改选择器的时候,只有完全匹配该特性才会生效。问题在于大量的元素和特性组合的选择器 (比如 button[data-v-f3f3eg9]) 会比类和特性组合的选择器慢,所以应该尽可能选用类选择器。 )
<style scoped>
...
</style>
1
2
3

# 6. css 规范

参考百度前端团队的css规范,并搭配stylelint作为语法检查,详情查看以下有关文档。

示例请查看示例文件夹下的css编写示例

fex团队css规范

stylelint—css代码检查工具

css资源大全中文版

# 命名

  • class 名称中只能出现小写字符和破折号(dashe)。破折号应当用于相关 class 的命名(例如,.btn 和 .btn-danger)。
  • 避免过度任意的简写。.btn 代表 button,但是 .s 不能表达任何意思。
  • class 名称应当尽可能短,并且意义明确。
  • 基于最近的父 class 或基本(base) class 作为新 class 的前缀
  • 编写公用样式时,应尽量做到不把页面样式耦合。合理的拆分样式,做到更灵活应用。

1.1 常用命名

页面:wrapper 页头: header 页面主体: main 页脚:footer 广告: banner 导航: nav 子导航: subnav 菜单: menu 子菜单: submenu 搜索: search 滚动: scroll ...

2.2 合理拆分样式

.btn-primary {
	font-size: 12px;
	color: #333333;
}
.btn-success {
	color: #f9ec00;
}
.btn-error {
	color: #f13d3d;
}

.flex-box{
    display: flex;
}
.flex-item{
    flex: 1;
}
.flex-align{
    justify-content:center;
    align-items:center;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 书写规范

1.【缩进】:使用 4 个空格做为一个缩进层级。

.selector {
    margin: 0;
    padding: 0;
}
1
2
3
4

2.【空格】

  • 选择器 与 { 之间必须包含空格。
  • 属性名 与之后的 : 之间不允许包含空格, : 与 属性值 之间必须包含空格。
  • 列表型属性值 书写在单行时,, 后必须跟一个空格。
.selector {
    margin: 0;
    font-family: Arial, sans-serif;
}
1
2
3
4

3.【换行】

  • 当一个 rule 包含多个 selector 时,每个选择器声明必须独占一行。
.post,
.page,
.comment {
    line-height: 1.5;
}
1
2
3
4
5

4.【属性】

  • 属性定义必须另起一行。
  • 属性定义后必须以分号结尾。
/* good */
.selector {
    margin: 0;
    padding: 0;
}
/* bad */
.selector { margin: 0; padding: 0; }
1
2
3
4
5
6
7

5.【嵌套】:使用sass、less等编写样式时,嵌套最多不超过3层

.wrapper{
    .container{
        .title{
            color: red;
        }
    }
}
1
2
3
4
5
6
7

6.更多请看上面有关文档介绍。。。

# 7. 示例

# 项目目录结构

目录结构

  • public // 静态资源文件
  • src
    • assets // 图片等资源
    • common // 公用环境变量
      • components // 公用组件
    • Header
      • index.vue
      • index.js
      • index.css
      • directives // 全局directive
      • filters // 全局filter
  • pages // 业务组件
    • demo-one
      • components
        • GoodsList -index.vue
          • goods-list.css
      • index.vue
      • plugins // 全局插件
      • store // vuex状态管理
      • styles // 公用样式
    • app.css // import所有的公用样式
    • common.css // 全局公用样式文件
    • normalize.css // css reset(第三方文件,最好不好修改)
    • variable.json // postcss 变量文件
      • utils // 工具类js
  • .env.development // 环境变量文件
  • .env.production // 环境变量文件
  • .env.test // 环境变量文件
  • vue.config.js // webpack等配置文件

# vue文件编写示例

父组件

<template>
	<div>
		<goods-list goods-status="111" @get-goods-id="getGoodsId"/>
	</div>
</template>
<script>
import GoodList from './GoodsList'
export default{
	name: 'GoodCategory',
    components: {
		GoodList
	}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

子组件

<template>
    <div class='demo-wrapper'>
        <!-- 元素特性顺序 -->
        <ul class="demo-list">
            <li class="list-item"
                v-for='item in itemList'
                :key='item.id'
                ref="`item_`${item.id}"
                v-modle='keyword'
				@click="emitGoodsId(item.id)">
                <span>{{item.name}}</span>
            </li>
        </ul>
    </div>
</template>
<script>
export default {
	// 声明
    name: 'GoodsList',
	// 模板依赖
    components: {},
    directives: {},
    filters: {},
	// 组合(向内合并属性)
    extends: {},
    mixins: {},
	// 接口
    props: {
        isDayUse: {
            required: true,
            type: Boolean,
            default: false
        }
    },
	// 本地的响应式属性
    data: function() {
        return {}
    },
    computed: {},
	// 事件
    watch: {},
	// 生命周期
    beforeCreate() {},
    created() {},
    beforeMount() {},
    mounted() {},
    beforeUpdate() {},
    updated() {},
    activated() {},
    deactivated() {},
    beforeDestroy() {},
    destroyed() {},
    methods: {
		emitGoodsId(id) {
			this.$emit('get-goods-id', id);
		}
	}
}
</script>
<style scoped>
.demo-list{
    padding: 10px;
}
</style>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

# css编写示例

/* 
* flex 
*/
.flex-wrapper{
    display: flex;
}
.flex-item{
    flex: 1;
}
.flex-align{
    justify-content:center;
    align-items:center;
}
/* 
* 文字排版
*/
.text-right{
    text-align: right!important;
}
.text-left{
    text-align: left!important;
}
.text-center{
    text-align: center!important;
}
/*
* 文字颜色
*/
.color-primary{
	color: #ff0000;
}
.color-primary-light{
	color: #f20000;
}
.color-primary-darker{
	color: #ff40000;
}
.color-success{
	color: #f3f3f3;
}
.color-error{
	color: #e60a0a;
}
/*
* 布局排版 - 垂直居中
*/
.position-center-wrap{
    position: relative;
}
.position-center{
    position:absolute;
    top:50%;
    left: 50%;
    transform: translate(-50%, -50%);
}
/* 
* 浮动
*/
.pull-left{
    float: left!important;
}
.pull-right{
    float: right!important;
}
.clearfix:before,
.clearfix:after {
    content: "";
    display: table;
}
.clearfix:after {
    clear: both;
}
.clearfix {
    zoom: 1; /* ie 6/7 */
}
/* 
* 图片自适应
*/
.img-responsive {
	max-width: 100%;
	width: 100%;
	height: auto;
	display: block;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84

# css常用命名

  • about 关于
  • account 账户
  • arrow 箭头图标
  • article 文章
  • aside 边栏
  • audio 音频
  • avatar 头像
  • bg,background 背景
  • bar 栏(工具类)
  • branding 品牌化
  • crumb,breadcrumbs 面包屑
  • btn,button 按钮
  • caption 标题,说明
  • category 分类
  • chart 图表
  • clearfix 清除浮动
  • close 关闭
  • col,column 列
  • comment 评论
  • community 社区
  • container 容器
  • content 内容
  • copyright 版权
  • current 当前态,选中态
  • default 默认
  • description 描述
  • details 细节
  • disabled 不可用
  • entry 文章,博文
  • error 错误
  • even 偶数,常用于多行列表或表格中
  • fail 失败(提示)
  • feature 专题
  • fewer 收起
  • field 用于表单的输入区域
  • figure 图
  • filter 筛选
  • first 第一个,常用于列表中
  • footer 页脚
  • forum 论坛
  • gallery 画廊
  • group 模块,清除浮动
  • header 页头
  • help 帮助
  • hide 隐藏
  • hightlight 高亮
  • home 主页
  • icon 图标
  • info,information 信息
  • last 最后一个,常用于列表中
  • links 链接
  • login 登录
  • logout 退出
  • logo 标志
  • main 主体
  • menu 菜单
  • meta 作者、更新时间等信息栏,一般位于标题之下
  • module 模块
  • more 更多(展开)
  • msg,message 消息
  • nav,navigation 导航
  • next 下一页
  • nub 小块
  • odd 奇数,常用于多行列表或表格中
  • off 鼠标离开
  • on 鼠标移过
  • output 输出
  • pagination 分页
  • pop,popup 弹窗
  • preview 预览
  • previous 上一页
  • primary 主要
  • progress 进度条
  • promotion 促销
  • rcommd,recommendations 推荐
  • reg,register 注册
  • save 保存
  • search 搜索
  • secondary 次要
  • section 区块
  • selected 已选
  • share 分享
  • show 显示
  • sidebar 边栏,侧栏
  • slide 幻灯片,图片切换
  • sort 排序
  • sub 次级的,子级的
  • submit 提交
  • subscribe 订阅
  • subtitle 副标题
  • success 成功(提示)
  • summary 摘要
  • tab 标签页
  • table 表格
  • txt,text 文本
  • thumbnail 缩略图
  • time 时间
  • tips 提示
  • title 标题
  • video 视频
  • wrap 容器,包,一般用于最外层
  • wrapper 容器,包,一般用于最外层
上次更新时间: 8/4/2020, 8:24:44 PM