Riot.js 代码风格指南 · 简单心理技术团队

2,237 阅读8分钟
Riot.js 是一个比 React.js 和 Vue.js 更轻量的前端框架。但作为灵活的代价,团队协作时需要一份代码风格指南以保证代码风格的一致。

本文节选翻译自 github.com/voorhoede/r…

宗旨

本指南的目标是提供一份统一的 Riot.js 代码风格指南,以使你的项目达到以下效果:

  • 帮助开发人员理解和查找代码;
  • 方便 IDE 高亮代码和提供协助;
  • 方便构建工具构建代码;
  • 方便缓存、打包和分离代码。

本指南受 John Papa 的 AngularJS Style Guide 启发。

示例

这些是按照本指南编写的示例项目:voorhoede.github.io/riotjs-demo…

正文

模块化开发

让你的代码模块化,并保证其业务逻辑小而清晰。

模块是应用的组成部分,Riot.js 可以方便的构建和组织模块。

为什么要这么做?

无论对于你还是他人,小模块都是便于阅读、理解、维护、重用和调试的最佳选择。

如何做?

每个模块都需要符合 FIRST 原则:专注(Focused,单一职责)、独立(Independent)、可复用(Reusable)、简洁(Small)和可测试(Testable)。

如果你的模块功能太多,导致体积臃肿,请把它切分成多个小模块。比如确保每个模块的代码不超过 100 行,并使其与其它模块隔离。

小贴士

如果你使用 AMD 或 CommonJS 来加载模块,可以在命令行中加上--modular参数

# enable AMD and CommonJS
riot --modular

模块的命名

模块的名字代表模块的用途,需要遵守以下要求:

  • 恰当的含义:避免过于具体或过于抽象;
  • 简短:2 ~ 3 个单词;
  • 顺口:不绕口,方便交流;
  • 符合 W3C 的自定义标签标准,使用连字符,避开保留名称;
  • 以对象作为名字的前缀。除非模块非常通用,否则不要只使用一个词作为名字。

为什么要这么做?

  • 模块间通过名字来通讯,所以名字必须有恰当的含义、简短且顺口;
  • 名字将作为标签名写入 HTML 中,所以必须符合 HTML 的规范。

如何做?


 />
 />
 />


 /> 
 /> 
 /> 

一个模块一个文件夹

构建时把一个模块的所有文件打包成一个文件。

为什么这么做?

打包文件方便查找和重用。

如何做?

把模块名作为文件夹的名字和文件前缀,后缀为文件类型。

modules/
└── my-example/
    ├── my-example.tag.html
    ├── my-example.less
    ├── ...
    └── README.md

如果你的模块里有子模块,可以把子模块作为子文件夹。

modules/
├── radio-group/
|   └── radio-group.tag.html
└── search-form/
    ├── search-form.tag.html
    ├── ...
    └── search-filters/
        └── search-filters.tag.html

使用 .tag.html 作为文件后缀

Riot.js 建议使用 .tag 作为模块的文件名后缀,虽然它的本质是一个自定义标签。

为什么这么做?

  • 告诉开发者这不仅仅是一个 html 文件,而是 Riot.js 模块;
  • 帮助 IDE 识别文件类型。

如何做?

如果在浏览器端使用,可以这么写:

src="path/to/modules/my-example/my-example.tag.html" type="riot/tag">

预编译时,需要加上后缀参数

riot --ext tag.html modules/ dist/tags.js

如果你使用 Webpack tag loader,需要设置 loader

{test:/\.tag.html$/,loader:'tag'}

在自定义标签里使用

The year is { this.year }

this.year = (new Date()).getUTCFullYear();

保持自定义标签中的逻辑简洁

Riot.js 提供的标签内置语法支持 Javascript 语法,但这并不表示建议你在其中编写复杂的代码。

为什么这么做?

  • 复杂的内置脚本会导致难以阅读;
  • 复杂的内置脚本难以重用;
  • IDE 无法准确识别内置脚本。

如何做?

把复杂的内置脚本移到模块变量或模块方法里。



    { year() + '-' + month() }

    




    { (new Date()).getUTCFullYear() + '-' + ('0' + ((new Date()).getUTCMonth()+1)).slice(-2) }

保持参数简洁

Riot.js 支持通过自定义属性给模块添加参数,比如 ,可以在模块中通过 opts.MyAttr 获取参数。

尽管 Riot.js 支持使用复杂的原生 Javascript 语法来传递参数,但我们应保持参数的简洁,只使用标准的Javascript 类型,如字符串、数字、函数等。

允许的例外情况是只能使用复杂对象来传递参数(如一组对象、递归模块等)或通用的业务对象(如 Product)。

为什么这么做?

  • 每个属性作为一个独立参数,可以让参数易于阅读和理解;
  • 只使用默认的 Javascript 类型可以让代码风格更接近 HTML 的原生风格;
  • 复杂的参数会导致难以理解和重构,导致技术债务。

如何做?

每个属性作为一个独立的参数,参数值为 Javascript 原生类型。









    { opts.text }
    

初始化参数

在 Riot.js 中,参数是模块的 API。一个健壮的参数初始化过程,可以方便他人使用你的模块。

模块的参数可能是 Riot.js 的表达式(attr="{ var }")、纯字符串(attr="value")或不存在。你需要初始化处理这些情况。

为什么这么做?

初始化参数可以确保你的模块总是可以正常运行。哪怕其他开发者将其用于超出你预期的用途。

如何做?

  • 给参数设置默认值;
  • 使用类型转换,将值转换成期望的类型;
  • 在使用参数前,先检查参数是否存在。

Riot.js 的示例 中,可以把代码进行如下改进:

this.items = opts.items || []; // 设置默认值为空数组

这样改进后,可以让模块在以下情况中正常运作:

  
 

对于 这种模块,我们预期其参数都是数字类型,可以这么写:

// 如果没有传入 step,就设为默认值
this.step = !isNaN(Number(opts.step)) ? Number(opts.step) : 1;

以此确保以下情况都可以正常运行:

 
 
 

模块支持可选的 on-slide 事件时,需要在使用前确认是否传入了回调:

slider.on('slide', (values, handle) => {
    if (typeof opts.onSlide === 'function') {
        opts.onSlide(values, handle);
    }
}

以此确保以下情况可以正常运行:

 
 
 

将 this 改名为 tag

在 Riot.js 模块内,this 指向模块实例,把 tag 赋值为 this 后,可以在不同的作用域下调用模块实例。

为什么这么做?

tag 赋值为实例变量,其它开发者就可以方便的调用了。

如何做?

/* 推荐 */

// ES5 的赋值写法
var tag = this;
window.onresize = function() {
    tag.adjust();
}

// ES6 中可以把 tag 赋值为常量
const tag = this;
window.onresize = function() {
    tag.adjust();
}

// ES6 中也可以使用 => 来继续使用 this
window.onresize = () => {
    this.adjust();
}

/* 避免 */
var self = this;
var _this = this;
// 等等

在顶部声明变量

在 Riot.js 的模块中,你可以任意声明变量和方法,但这会导致可读性问题。所以建议把变量和方法按顺序在顶部声明。

为什么这么做?

  • 在顶部声明变量和方法,可以方便开发者得知可以使用哪些变量和方法;
  • 按字母顺序排列可以便于查找;
  • 将方法放在后面,可以隐藏执行细节,便于一瞥全局。

如何做?

把变量声明和方法移到顶部。

/* 推荐 */
var tag = this;
tag.text = '';
tag.todos = [];
tag.add = add;
tag.edit = edit;
tag.toggle = toggle;

function add(event) {
    /* ... */
}

function edit(event) {
    /* ... */
}

function toggle(event) {
    /* ... */
}   

/* 避免 */
var tag = this;

tag.todos = [];
tag.add = function(event) {
    /* ... */
}

tag.text = '';
tag.edit = function(event) {
    /* ... */
}

tag.toggle = function(event) {
    /* ... */
}

你也可以把 mixinsobservables 放在顶部:

/* recommended */
var tag = this;
// alphabetized properties
// alphabetized methods
tag.mixin('someBehaviour');
tag.on('mount', onMount);
tag.on('update', onUpdate);
// etc

避免使用非标准的 ES6 语法

Riot.js 支持模仿 ES6 的方法声明语法,把 methodName() { } 编译成 this.methodName = function() {}.bind(this)。但这并不是 ES6 的标准语法。

为什么这么做?

  • 这并不是 ES6 的标准语法,会造成开发者的困扰;
  • IDE 无法准确识别这种语法;
  • 这种语法无法清晰表达其作用。

如何做?

使用 tag.methodName = 替代 methodName() { }

/* 推荐 */
var tag = this;
tag.todos = [];
tag.add = add;

function add() {
    if (tag.text) {
        tag.todos.push({ title: tag.text });
        tag.text = tag.input.value = '';
    }
}

/* 避免 */
todos = [];

add() {
    if (this.text) {
        this.todos.push({ title: this.text });
        this.text = this.input.value = '';
    }
}

小贴士

可以加上禁用 ES6 的参数,来避免编译这种语法:

riot --type none

避免使用 tag.parent

Riot.js 支持嵌套模块,可以通过 tag.parent 访问父模块。但这种行为违反了 FIRST 原则,应当避免使用。

例外的情况是在循环中,且子对象是匿名模块。

为什么这么做?

  • 每个模块需运行在自己的作用域中,不同模块间保持隔离;
  • 如果一个模块需要访问父模块,那它将无法在不同的环境下复用;
  • 允许访问和修改父模块,可能会导致不可预期的错误。

如何做?

  • 把需要的值从父模块传入子模块;
  • 父模块以回调的形式监听子模块的事件,以修改自身。


     



    { opts.value } 




    



    value: { parent.value } 




     
    



     




    
    



     




     
        { item.text }
    
    

使用 each ... in 语法

Riot.js 支持多种循环的语法:数组可以写成 each="{ item in items }";对象可以写成 each="{ key, value in items }";以及简写 each="{ items }"。但这种简写会导致混淆,所以建议用 each ... in

为什么这么做?

当 Riot.js 执行循环语句时,会把当前对象放到 this 中,这种做法并不直观,可能会引起开发者的困扰。

如何做?

使用 each="{ item in items }"each="{ key, value in items }" 替代 each="{ items }"


  • { item.title }
  • { key }. { item.title }
  • { title }

将样式放在独立的文件

为了给开发者提供便利,Riot.js 允许在模块中嵌套 </code></a>,并提供了 <a href="http://riotjs.com/guide/#scoped-css">scope</a> 功能来限制样式的作用域,但这并不是真正的样式隔离。</p> <p><strong>为什么这么做?</strong></p> <ul> <li>独立的样式文件可以让浏览器更容易的读取,并且避免因模块加载出错或者未加载而导致问题;</li> <li>独立的样式文件可以方便的进行预编译(如 Sass、PostCSS 等);</li> <li>独立的样式文件可以独立地进行压缩、构建和缓存,这样有利于提升性能;</li> <li>Riot.js 并没有额外的功能提供给内嵌样式。</li> </ul> <p><strong>如何做?</strong></p> <p>将样式拆分为独立文件放在模块文件夹内:</p> <pre><code>my-example/ ├── my-example.tag.html ├── my-example.(css|less|scss) <-- external stylesheet next to tag file └── ... </code></pre> <h4>使用标签名字作为样式的作用域</h4> <p>Riot.js 的模块是一个自定义标签,标签名非常适合作为样式的作用域。</p> <p><strong>为什么这么做?</strong></p> <ul> <li>使用标签名作为样式的作用域,可以让样式效果更可预期;</li> <li>统一的名字便于开发者理解。</li> </ul> <p><strong>如何做?</strong></p> <p>使用标签名作为样式的父类或作用域。</p> <pre><code>/* 推荐 */ my-example { } my-example li { } .my-example__item { } /* 避免 */ .my-alternative { } /* not scoped to tag or module name */ .my-parent .my-example { } /* .my-parent is outside scope, so should not be used in this file */ </code></pre> <p><strong>小贴士</strong></p> <p>如果使用 2.3.17 之后的版本,可以使用 <a href="http://riotjs.com/guide/#html-elements-as-tags"><code>[data-is="my-example"]</code></a> 来替代 <code>.my-example</code></p> <h4>为模块添加说明文档</h4> <p>一个模块包括了属性和方法,为了便于其他开发人员使用,需要写一份文档来说明这些属性和方法。</p> <p><strong>为什么这么做?</strong></p> <ul> <li>文档帮助开发者快速了解模块的整体情况,而不需要阅读具体的代码。这使得模块更易于使用;</li> <li>文档里标明了模块的属性信息,可以让只想使用(而非开发)它的人快速上手;</li> <li>文档帮助开发者理清哪些内容是对外暴露的,更新时需要考虑兼容性;</li> <li><code>README.md</code> 是标准的说明文件格式,像 Github 之类的代码托管工具,可以方便的展示和阅读这份文件。</li> </ul> <p><strong>如何做?</strong></p> <p>添加 <code>README.md</code> 到模块文件夹:</p> <pre><code>range-slider/ ├── range-slider.tag.html ├── range-slider.less └── README.md </code></pre> <p>在文档中,描述该模块的功能和使用方法,并说明其自定义属性和方法的含义和用法。</p> <pre><code># Range slider ## Functionality The range slider lets the user to set a numeric range by dragging a handle on a slider rail for both the start and end value. This module uses the [noUiSlider](http://refreshless.com/nouislider/) for cross browser and touch support. ## Usage `<range-slider>`</range-slider> supports the following custom tag attributes: | attribute | type | description | --- | --- | --- | `min` | Number | number where range starts (lower limit). | `max` | Number | Number where range ends (upper limit). | `values` | Number[] *optional* | Array containing start and end value. E.g. `values="[10, 20]"`. Defaults to `[opts.min, opts.max]`. | `step` | Number *optional* | Number to increment / decrement values by. Defaults to 1. | `on-slide` | Function *optional* | Function called with `(values, HANDLE)` while a user drags the start (`HANDLE == 0`) or end (`HANDLE == 1`) handle. E.g. `on-slide={ updateInputs }`, with `tag.updateInputs = (values, HANDLE) => { const value = values[HANDLE]; }`. | `on-end` | Function *optional* | Function called with `(values, HANDLE)` when user stops dragging a handle. For customising the slider appearance see the [Styling section in the noUiSlider docs](http://refreshless.com/nouislider/more/#section-styling). </code></pre> <h4>增加模块示例</h4> <p>增加 <code>*.demo.html</code> 示例文件,来表示模块该如何被使用。</p> <p><strong>为什么这么做?</strong></p> <ul> <li>模块示例证明该模块可以被独立使用;</li> <li>模块示例让使用者在查看文档和代码前就对模块有个大体概念;</li> <li>示例中可以展示该模块所有可能的使用方法。</li> </ul> <p><strong>如何做?</strong></p> <p>添加 <code>*.demo.html</code> 文件到模块文件夹:</p> <pre><code>city-map/ ├── city-map.tag.html ├── city-map.demo.html ├── city-map.css └── ... </code></pre> <p>在示例文件中,需要:</p> <ul> <li>引入 <code>riot+compiler.min.js</code> 来解析和执行示例;</li> <li>引入模块文件,如 <code>./city-map.tag.html</code>;</li> <li>创建一个 <code>demo</code> 标签用于嵌入模块;</li> <li>在 <code><demo></code> 中编写示例;</li> <li>可以给 <code>demo</code> 标签加上 <code>aria-label</code> 属性来说明示例的内容;</li> <li>使用 <code>riot.mount('demo', {})</code> 来初始化。</li> </ul> <p>下面是一个例子:</p> <pre><code><!-- modules/city-map/city-map.demo.html: --> <body> <h1>city-map demos</h1> <demo aria-label="City map of London"> <city-map location="London" /> </demo> <demo aria-label="City map of Paris"> <city-map location="Paris" /> </demo> <link rel="stylesheet" href="./city-map.css"> <script src="path/to/riot+compiler.min.js"></script> <script type="riot/tag" src="./city-map.tag.html"></script> <script> riot.tag('demo','<yield/>'); riot.mount('demo', {}); </script> <style> /* add a grey bar with the `aria-label` as demo title */ demo:before { content: "Demo: " attr(aria-label); display: block; background: #F3F5F5; padding: .5em; clear: both; }

检查模块文件的代码风格

检查工具可以改进代码风格的一致性并找出语法错误。通过一些额外的配置,我们可以检查 Riot.js 模块的代码风格。

为什么这么做?

  • 保证开发者们有一致的代码风格;
  • 提前发现语法错误。

如何做?

使用 ESLintJSHint 来检查代码风格。

给你的项目加上 RiotJS Style Guide 标识

给项目加上标识,并链接到本指南。

RiotJS Style Guide badge

为什么这么做?

让其他开发人员知道和了解本指南。

怎么做?

在 markdown 文件中引入:

[![RiotJS Style Guide badge](https://cdn.rawgit.com/voorhoede/riotjs-style-guide/master/riotjs-style-guide.svg)](https://github.com/voorhoede/riotjs-style-guide)

在 html 文件中引入:

 
    RiotJS Style Guide badge