V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
ksco
V2EX  ›  分享创造

八个简单规则构建健壮可扩展的 CSS 架构

  •  4
     
  •   ksco · 2017-02-27 08:29:48 +08:00 · 2870 次点击
    这是一个创建于 2608 天前的主题,其中的信息可能已经有所发展或是发生改变。

    前端新人,昨天下午看到一篇不错的文章,就翻译了一下,文中“我”都是指的原作者。

    原项目地址:8 simple rules for a robust, scalable CSS architecture

    这些是我身为专业前端工程师多年来在大型的复杂项目中总结出的一些关于管理 CSS 的经验。因为被别人问过太多遍,所以我觉得将其整理成文档是个好主意。

    我已经尽量保持行文简练,但下面还是给出一个太长不看版:

    1. 尽量使用类选择器
    2. 合并组件代码
    3. 使用统一的类命名空间
    4. 保持命名空间和文件命名的严格对应
    5. 保证组件内的样式不外泄
    6. 保证组件内的样式不内漏
    7. 谨慎处理组件的边界
    8. 避免与外部样式库耦合

    介绍

    如果你在编写前端应用,总免不了跟样式打交道。尽管现在前端的技术百花齐放、日新月异, CSS 仍然是 WEB 端编写样式的唯一选择。目前主要有两种解决方案:

    • CSS 预处理器,这个技术已经出现很久了,例如 SASSLESS
    • CSS-in-JS 库,这是个相对较新颖的方式,例如 free-style

    这两种方法各有利弊,关于选择问题可以单独写一篇文章了。本文将更加专注于第一种方法,如果你偏好于后者,那么这篇文章可能就没那么有意思了。

    高层次目标

    那么标题说的健壮的可扩展的 CSS 架构具体指的是什么呢?

    • 面向组件 - 降低 UI 复杂度的最佳实践就是将 UI 分成较小的组件。如果你正在使用有品位的框架,那么这个框架的 Javascript 部分一定是符合这个准则的。例如 React 就鼓励你进行组件化和区块化。我们想让 CSS 也切合这个目标。
    • 沙盒化 - 如果修改了一个组件的样式会影响到其他组件,那么将 UI 分成组件并不会帮我们减轻认知负荷。一些 CSS 的基本特性,例如级联、全局命名空间等对都沙盒化起到了反作用。如果你熟悉 Web 组件规范的话,可以把这条准则类比作 Shadow DOM 的样式隔离,并且不需要考虑浏览器的兼容性,当然也不用担心这个规范是否会变成标准。
    • 无门槛 - 我们不需要为了迎合架构而牺牲自己的开发体验。如果可以,我们希望使整个过程变得更加优雅。
    • 安全性 - 某种程度上和上面的论点相关,我们想让所有的样式都默认是局部的,全局只能是例外情况。工程师是最懒的一批人,所以我们需要保证简单的做法就是正确的做法。

    具体规则

    1. 尽量使用类选择器

    这是最显而易见的一个论点。

    不要使用 ID 选择器(例如 #header),因为无论何时,你觉得某个东西只会有一个实例,随着时间的流逝,你都会被证明是错误的。

    比如有一次,我们想要找出一个当时正在做的大型应用中所有数据绑定错误。于是我们启动了两套 UI 实例,将其并排放在 DOM 中,两者都绑定到同一个共享的数据模型。这是为了确保数据模型中的所有改动都可以正确的映射到两套 UI 中。在这种情况下,所有之前假定唯一的组件(例如标题栏等)都不成立了。另外,这也是检测其他唯一性假定所导致的奇怪 BUG 的好方法。有点跑题,但这个故事寓意在于,任何可以使用 ID 选择器的地方都不如使用类选择器好,那干脆就不要用它了。

    另外,你也不应该直接使用标签选择器(例如 p),通常来说在组件中使用是允许的(后面会有提及),但不要单独使用,因为你总会在某个不需要这些样式的组件中取消它们。另外这也不符合我们刚才提到的高层次目标:面向组件、规避级联、默认局部化。如果需要在 body 上设置字体、行高、颜色(即继承属性)等,也不违背此规则。但如果你想严格遵循组件隔离,放弃这些也是完全可行的(具体见第 8 条)。

    所以除了少数的例外情况,应该总是使用类选择器

    2. 合并组件代码

    当你编写某个组件的时,如果所有相关的东西( Javascript 、样式、测试、文档等)都放在一起会非常有帮助:

    ui/
    ├── layout/
    |   ├── Header.js              // 组件代码
    |   ├── Header.scss            // 组件样式
    |   ├── Header.spec.js         // 组件相关的单元测试
    |   └── Header.fixtures.json   // 单元测试可能会用到的模拟数据
    ├── utils/
    |   ├── Button.md              // 组件的使用文档
    |   ├── Button.js              // ...等等
    |   └── Button.scss
    

    当你写代码的时候,打开项目浏览工具,所有跟这个组件相关的东西都尽在指尖。样式和产生 DOM 的 Javascript 代码生来就紧密相关,很大程度上你写一会 CSS 就会去编写相应的 Javascript ,反之亦然。对于组件和对应的测试文件也是同理。你可以把这个当作是 UI 组件的局部性原理。我曾经也小心翼翼的将我的代码分放到 styles/tests/docs/ 等目录下,直到后来我才意识到,这样做的原因只是因为习惯而已,并没任何实质性的好处。

    3. 使用统一的类命名空间

    对于类以及其他的标记符(如 ID 、动画名称等), CSS 只有一个全局的、扁平的命名空间。与老版本的 PHP 类似,社区普遍使用冗长的、结构化的名字来模拟命名空间(BEM 就是其中之一)。我们也需要选择一种命名空间约定,并且遵守它。

    举个例子,比如说我们使用 myapp-Header-link 作为类名,三个部分都有具体的职责:

    • myapp 确保将我们的应用和同 DOM 下的其他应用隔离
    • Header 将当前组件和同应用下的其他组件隔离
    • link 作为组件的命名空间下的一个局部名字为组件提供样式

    作为特殊情况,Header 组件的根元素可以简单的命名为 myapp-Header 类。对于一个简单的组件来说,可能只需要一个根元素就够了。

    不管选择什么样的命名空间约定,我们都需要保证始终如一。上面提到的三个部分不止有各自的职责,也有具体的意义。只需要看一眼这个类的名字,就可以知道它属于什么地方。命名空间也是我们组织项目样式的方式。

    这篇文章中将会使用 app-Component-class 的命名方式,我个人觉得这种方式很实用,当然你也可以选择一种更加适合自己的。

    4. 保持命名空间和文件命名的严格对应

    这条规则只是前面两条(合并组件代码和类命名空间)的逻辑结合:所有跟组件相关的样式代码都应该放到以该组件命名的文件中,没有例外

    如果你在浏览器中发现了某个组件没有正常工作,就可以右键审查元素,你可能会看到如下代码:

    <div class="myapp-Header">...</div>
    

    看到组件的名字,切换到代码编辑器,按下“快速打开文件”的快捷键,输入“ head ”,就可以看到:

    Quick open file

    如果你刚刚加入某个团队,不太熟悉代码的架构,这种严格的对应方式会变得更加有用:你不需要特别清楚整个项目的核心就可以快速完成工作。

    这样做有一个自然的,但也许不是显而易见的推论:一个独立的样式文件应该只包含一个单独的组件。为什么?假设我们有一个登录表单组件,这个组件只会在 Header 组件中用到。在 Javascript 中,它被作为 Header.js 的一个辅助组件,没有被导出到其他地方。我们可能会给这个组件起名为 myapp-LoginForm,并把它的相关代码塞到 Header.jsHeader.scss 中。

    然后假设团队中来了个新人,负责修复登录表单中一个小布局问题。他检查 DOM 元素找到了问题所在。但文件目录中并没有 LoginForm.jsLoginForm.scss 存在,所以他就不得不使用 grep 等搜索工具或靠猜测来找到相关的文件。也就是说,如果登录表单是一个单独的组件,就把它放到不同的文件中,这在非小型的项目中是非常重要的。

    5. 保证组件内的样式不外泄

    既然已经确定了命名空间的规范,我们就可以利用它们将 UI 组件进行分离了。如果可以保证每个组件只使用自己的唯一前缀的类名,就可以保证组件内的样式不会泄漏给其他的组件。这种做法非常有效(后面会有注意事项),但不可避免的需要不断地输入命名空间,难免心累。

    一个健壮的,同时非常简单的方法是把整个样式文件放到一个前缀块中。下面的代码仅需要写一次组件的名字:

    .myapp-Header {
      background: black;
      color: white;
    
      &-link {
        color: blue;
      }
    
      &-signup {
        border: 1px solid gray;
      }
    }
    

    上面的例子是使用 SASS 编写的,但比较棒的是 & 符号对于几乎所有的 CSS 预处理器(SASSPostCSSLESS 以及 Stylus)都适用。为了说明论点,上面的 SASS 编译后的代码如下:

    .myapp-Header {
      background: black;
      color: white;
    }
    
    .myapp-Header-link {
      color: blue;
    }
    
    .myapp-Header-signup {
      border: 1px solid gray;
    }
    

    这个也适用于大部分的模式,例如对于不同的组件状态使用不同的样式:

    .myapp-Header {
    
      &-signup {
        display: block;
      }
    
      &-isScrolledDown &-signup {
        display: none;
      }
    }
    

    编译后:

    .myapp-Header-signup {
      display: block;
    }
    
    .myapp-Header-isScrolledDown .myapp-Header-signup {
      display: none;
    }
    

    即使是媒体查询也非常方便,只要你使用的预处理器支持冒泡即可( SASS 、 LESS 、 PostCSS 以及 Stylus 都支持)

    .myapp-Header {
    
      &-signup {
        display: block;
    
        @media (max-width: 500px) {
          display: none;
        }
      }
    }
    

    编译后:

    .myapp-Header-signup {
      display: block;
    }
    
    @media (max-width: 500px) {
      .myapp-Header-signup {
        display: none;
      }
    }
    

    上面的模式可以让我们非常方便地使用略显冗长的唯一类名,而不用一遍一遍地反复输入。这种便利性是非常必要的,因为如果写起来很麻烦,程序猿就会偷工减料。

    偷偷提一下 JS 相关的内容

    这篇文章说的是样式上的约定,但样式也不是孤岛般的存在,在 JS 端我们也需要处理相同的命名空间化的类名,所以便利性也非常重要。

    羞耻的插个广告,我为此编写了一个非常简单的、零依赖的 JS 库 css-ns ,当和框架相结合(比如 React)时,它允许你在文件中指定一个特定的命名空间:

    // Create a namespace-bound local copy of React:
    var { React } = require('./config/css-ns')('Header');
    
    // Create some elements:
    <div className="signup">
      <div className="intro">...</div>
      <div className="link">...</div>
    </div>
    

    编译到 DOM 中的结果:

    <div class="myapp-Header-signup">
      <div class="myapp-Header-intro">...</div>
      <div class="myapp-Header-link">...</div>
    </div>
    

    这是非常方便的,上面的代码也将 JS 代码默认局部化了。

    好吧我又跑题了,回到 CSS 上。

    6. 保证组件内的样式不内漏

    还记得我之前说在每个类名前面加上组件的命名空间是一个非常有效的沙盒化做法吗?还记得我说会有注意事项吗?

    考虑下面的样式:

    .myapp-Header {
      a {
        color: blue;
      }
    }
    

    在下面的组件继承中:

    +-------------------------+
    | Header                  |
    |                         |
    | [home] [blog] [kittens] | <-- 这些都是 <a> 元素
    +-------------------------+
    

    没什么问题,对吗?只有在 Header 内部的 <a> 是蓝色的,因为我们生成的规则是:

    .myapp-Header a { color: blue; }
    

    但假设布局之后变成了这样:

    +-----------------------------------------+
    | Header                    +-----------+ |
    |                           | LoginForm | |
    |                           |           | |
    | [home] [blog] [kittens]   | [info]    | | <-- 这些都是 <a> 元素
    |                           +-----------+ |
    +-----------------------------------------+
    

    选择器 .myapp-Header a 同时匹配了 LoginForm 内部的 <a> 元素,我们所谓的样式隔离的也就灰飞烟灭了。事实证明,把所有的样式放在一个命名空间块中可以有效隔离组件间的样式污染,但对于组件内部却是无能为力的

    这个问题有两种解决方案:

    1. 永远不要使用标签选择器。如果 Header 中的每个 <a> 都用 <a class="myapp-Header-link"> 替代,我们就永远不会有这个问题。但有时你使用了完美的语义化标签,<article><aside><th> 等,并且运用非常到位,你可能就不希望再画蛇添足地为其添加额外的类,对于这种情况:
    2. 对于非命名空间内的目标,只使用直接后代选择器 >

    针对后一种方法,我们可以把代码改成:

    .myapp-Header {
      > a {
        color: blue;
      }
    }
    

    这可以保证隔离可以在指定深度的组件树中工作,因为生成的选择器变成了 .myapp-Header > a

    这可能听起来有争议,那我举个更极端的例子来证明我的观点:

    .myapp-Header {
      > nav > p > a {
        color: blue;
      }
    }
    

    经过多年的可靠建议,我们都对选择器嵌套唯恐避之不及,包括直接后代选择器 >,但为什么呢?主要原因有下面三个:

    1. 级联样式所带来的邪恶终有一天会降临。你嵌套的选择器越多,某个选择器同时匹配了多个组件的可能性就越大。如果你从头读到了这里,你就知道我们已经规避了这个问题(使用严格的命名空间前缀、在需要的地方直接后代选择器)。
    2. 太具体的样式会降低可重用性。nav p a 除了在当前环境中,其他地方很难用上。但这篇文章就是为了避免这个问题,我们使用组件隔离了彼此,所以这个问题自然不存在。
    3. 太具体的样式会使得重构更加困难。这个在现实中是确实存在的:试想如果你只有一个 .myapp-Header-link a,那你可以在组件内把 <a> 放到任何地方,<a> 的样式总是可以匹配;但如果是 > nav > p > a,你就需要更新选择器以重新匹配 <a> 的新位置。但因为我们把 UI 组织成小的、互相隔离的组件,这个也不是什么问题。的确,如果在重构时,你需要考虑整个项目 HTML 和 CSS 代码,那确实很可怕。但如果只是针对一个小沙盒内的几十行代码,并且明确的知道无需考虑沙盒外部的东西,那也就构不成啥问题了。

    好奇心小贴士:阻止外部样式渗入组件

    到现在为止,我们是否达成了完美的沙盒目标,每个组件和页面的其他东西完全隔离呢?下面来快速回顾一下:

    • 我们通过对每个组件内的类添加命名空间前缀阻止了样式外泄

        +-------+
        |       |
        |    -----X--->
        |       |
        +-------+
      
    • 这也就意味了我们阻止了组件之间的样式泄漏

        +-------+     +-------+
        |       |     |       |
        |    ------X------>   |
        |       |     |       |
        +-------+     +-------+
    
    • 我们还通过限制子选择器阻止了组件的样式内漏

        +---------------------+
        |           +-------+ |
        |           |       | |
        |    ----X------>   | |
        |           |       | |
        |           +-------+ |
        +---------------------+
      
    • 但最重要的一点,外部的样式仍然可以渗入到我们的组件中

              +-------+
              |       |
        ---------->   |
              |       |
              +-------+
      

    举例来说,比如我们的组件样式如下:

    .myapp-Header {
      > a {
        color: blue;
      }
    }
    

    但我们引入了一个不符合约定的第三方库,定义了如下 CSS :

    a {
      font-family: "Comic Sans";
    }
    

    但不幸的是,并没有什么简单的办法保护你的组件不受外部侵害,对于这种情况,也许我们只能放弃挣扎。

    但幸运的是,你一般都可以选择自己的依赖库,所以只需要找一个更好的替代品即可。

    同时,我只是说没有简单的方法防止这种情况,这并不代表没有方法。事实上,有非常多的方法来解决这个问题,只是都要付出一些额外的代价:

    • 简单粗暴法:为每一个组件的每一个元素引入一个 CSS 样式重置,将其绑定到一个高优先级的选择器上就可以了。但除非你的程序非常小(例如一个给第三方用的分享按钮),不然这个方法会很快失控。这不是个好方法,放在这里只是为了全面性。
    • all: initial 是个不为人知的新 CSS 属性,它就是为这个问题而诞生的。 它可以 阻断属性的继承,也可以用来做局部重置。它的实现有些复杂,并且目前还没有被完全支持,但最终 all: initial 会成为一个样式隔离的有用工具。
    • Shadow DOM 前面已经提到过了,它是这个问题的完美解决方案,因为他允许对组件间的 JS 和 CSS 声明清晰的边界。尽管有点希望, Web 组件标准近几年没有很大的进展,除非你只针对特定支持的浏览器开发,否则不能依赖 Shadow DOM 。
    • 最后,还有 <iframe> 可供选择。它提供了 Web 运行时中最强的隔离性( CSS 和 JS ),但也拖慢了启动速度,提高了维护成本。但有些时候这点成本是值得的,其实很多著名的网页嵌入( Facebook 、 Twitter 、 Disqus 等)都是用 iframe 实现的。但对于我们而言,如果使用 iframe 隔离成千上万的小组件,这可能会让性能跌到谷底。

    总之,这个题外话有点跑远了,回到主题 CSS 规则中来:

    7. 谨慎处理组件的边界

    正如之前说的 .myapp-Header > a,当我们进行组件嵌套的时候,我们可能需要对子组件应用一些样式,考虑下面的布局:

    +---------------------------------+
    | Header           +------------+ |
    |                  | LoginForm  | |
    |                  |            | |
    |                  | +--------+ | |
    | +--------+       | | Button | | |
    | | Button |       | +--------+ | |
    | +--------+       +------------+ |
    +---------------------------------+
    

    显而易见的,使用 .myapp-Header .my-Button 并不是个好主意,而是需要使用.myapp-Header > .myapp-Button。但什么样式可以写在此处,而不是在子组件内部指定呢?

    注意到 LoginForm 是浮在 Header 的右侧的。凭直觉,此处样式可能是这样的:

    .myapp-LoginForm {
      float: right;
    }
    

    我们没有违反之前的任何规则,但我们已经让 LoginForm 非常难以重用了:如果后续的主页也想使用这个登录框,但不想浮在右侧,那就束手无策了。

    一个比较务实的解决方案是释放上面的约束,将其转移到具体使用它的组件中。具体来说,我们需要这样做:

    .myapp-Header {
      > .myapp-LoginForm {
        float: right;
      }
    }
    

    这样做没有任何问题,只要我们不乱来:

    // 反例; 不要这样做
    .myapp-Header {
      > .myapp-LoginForm {
        color: blue;
        padding: 20px;
      }
    }
    

    我们肯定不希望这样做,因为这打破了本地修改不间接影响全局安全保证。上面的代码中,LoginForm.scss 不再是修改组件样式时唯一需要检查的地方,会使我们又回到混乱的原点。所以怎么界定哪些属性在外部修改是 OK 的,哪些是禁止的呢?

    我们想要把沙盒保持在每个组件的内部,并且外部不需要知道组件的实现细节。它对我们来说是个黑盒子。子组件和父组件之间的界限来源于 CSS 中的某个基础概念:盒模型

    我的类比比较糟糕,但大致如此:就像是在某个国家的意思是身处这个国家的边界内部,我们也规定父组件只能修改子组件(直接后代)边界外部的属性。外部的意思是跟位置以及尺寸相关的属性(例如:positionmargindisplaywidthfloatz-index等),修改它们是允许的,但 border 内部的属性(包括 border 自身、paddingcolorfont 等)是不允许修改的。

    所以,下面的代码显示是不合法的:

    // 反例; 不要这样做
    .myapp-Header {
      > .myapp-LoginForm {
        > a { // relying on implementation details of LoginForm ;__;
          color: blue;
        }
      }
    }
    

    还有一些有趣的/无聊的边界情况,例如:

    • box-shadow - 阴影是组件样式的重要组成部分,所以组件内部应该有这个属性。但明显这个属性是在 border 之外的,所以它也属于父组件的管辖范围。
    • colorfont 和其他的[继承属性](]( https://developer.mozilla.org/en-US/docs/Web/CSS/inheritance)) - .myapp-Header > .myapp-LoginForm { color: red } 浸入了子组件的内部,但某种程度上来说,上面的代码功能上可以等同于 .myapp-Header { color: red; } ,而后者没有违反任何规则。
    • display - 如果子组件使用了 Flexbox 布局,它可能要求在根节点设置 display: flex 。但父节点可能使用 display:none 来对其进行隐藏。

    重要的是这些边界情况并不是危险区,只是把一点 CSS 级联重新引进你的样式中而已。相对于其他的坏味道,享受一点限制性的级联是没问题的。比如,仔细看看最后一个例子,优先级规则完美的解决了这个问题:当组件可见时,.myapp-LoginForm { display: flex } 是优先级最高的规则,所以这条规则生效。当父组件决定隐藏它时,.myapp-Header-loginBoxHidden > .myapp-LoginBox { display: none } 是优先级最高的规则,所以该规则生效,没毛病。

    8. 避免与外部样式库耦合

    为了避免重复工作,组件之间可能需要一个共享的样式;为了不从头造轮子,你可能也会选择第三方的样式库。这两种情况都应该避免给现有代码增添不必要的耦合。

    举个实际例子,假设我们需要使用 Bootstrap 的某些样式。因为其所有样式共享同一个全局的命名空间非常容易导致冲突, Bootstrap 作为一个非常蛋疼的样式库:

    • 向全局命名空间导出了大量的选择器(就 3.3.7 版本而言,具体是 2481 个),不管你是否使用它们。(有趣的小贴士: IE 9 之前的浏览器只能支持前 4095 个选择器,多余的就直接忽略了,我听说有人为此花费数天的时间尝试找出问题所在)
    • 使用硬编码的类名称如 .btn.table 等。无法想象这种名字不会碰巧被其他的开发者或项目使用。

    不管怎样,假设我们就想使用 Bootstrap 作为我们的 Button 组件的基础,不要再 HTML

    中编写:

    <button class="myapp-Button btn">
    

    而是考虑在你的样式中 extend 这个类:

    <button class="myapp-Button">
    
    .myapp-Button {
      @extend .btn; // from Bootstrap
    }
    

    这样做的好处是不在 HTML 组件中暴露给任何人荒谬的 btn 类。Button 可以把这个作为实现细节隐藏起来,外界不需要知道。这样的话,无论你是否用 Bootstrap 或者其他的库(或者全部由自己写),内部的改变外界都无法感知(除了界面上 Button 的外观变化)。

    这种做法同样适用于你自己写的助手类,而且你还可以选择一个更合理的名字:

    .myapp-Button {
      @extend .myapp-utils-button; // 在项目的其他地方定义
    }
    

    甚至完全不导出多余的类(大多数的预处理器都支持):

    .myapp-Button {
      @extend %myapp-utils-button; // 在项目的其他地方定义
    }
    

    最后,所有的 CSS 预处理器都支持 mixins,这是个非常强大的工具:

    .myapp-Button {
      @include myapp-generateCoolButton($padding: 15px, $withExplosions: true);
    }
    

    另外需要提及的一点是,对于更加开明的框架(例如 BourbonFoundation),它们都是这样做的:声明一堆的 mixins 供你根据需要选用,而不是产生一堆的样式污染命名空间。

    Neat.

    结束之前

    Know the rules, so you know when to break them

    打破成规

    最后,正如之前提到了,当你理解了你所拟定的规则(或者是采用了网络上的规则),你也可以允许一些例外情况,如果这样做更有意义的话。比如如果你觉得直接使用助手类有一些额外的价值的话,那就可以这样做:

    <button class="myapp-Button myapp-utils-button">
    

    这个额外的价值可能是你的自动测试框架可以更加智能地知道哪个元素是按钮,是否可以点击等。

    或者你可能觉得当改变非常小时,可以破坏组件之间的独立性,因为保持组件独立需要做大量的工作。我可能要提醒你这么做不太好,统一性非常重要。但只要你的团队没意见,你也完成了工作,那就没有任何问题。

    最后

    如果喜欢这篇文章,你可以向别人推荐它。

    License

    CC BY 4.0

    9 条回复    2017-03-03 17:23:58 +08:00
    watzds
        1
    watzds  
       2017-02-27 08:55:10 +08:00 via Android
    好!
    Bensendbs
        2
    Bensendbs  
       2017-02-27 09:33:00 +08:00
    👍
    yuuko
        3
    yuuko  
       2017-02-27 10:01:22 +08:00 via Android
    不错
    ksco
        4
    ksco  
    OP
       2017-02-27 13:32:19 +08:00
    charexcalibur
        5
    charexcalibur  
       2017-02-28 08:29:40 +08:00 via iPhone
    好。
    ksco
        6
    ksco  
    OP
       2017-03-01 13:18:09 +08:00
    45 收藏 4 回复惨案
    lingo
        7
    lingo  
       2017-03-03 11:24:20 +08:00
    @ksco 感谢居然比回复都多。心理平衡点了没。。
    ksco
        8
    ksco  
    OP
       2017-03-03 11:28:33 +08:00
    @lingo 没。。。
    zero1234888
        9
    zero1234888  
       2017-03-03 17:23:58 +08:00
    好东西!
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   3107 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 31ms · UTC 10:56 · PVG 18:56 · LAX 03:56 · JFK 06:56
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.