Riot.js 是一个比 React.js 和 Vue.js 更轻量的前端框架。但作为灵活的代价,团队协作时需要一份代码风格指南以保证代码风格的一致。
本文节选翻译自 https://github.com/voorhoede/riotjs-style-guide
本指南的目标是提供一份统一的 Riot.js 代码风格指南,以使你的项目达到以下效果:
本指南受 John Papa 的 AngularJS Style Guide 启发。
这些是按照本指南编写的示例项目:https://voorhoede.github.io/riotjs-demos/
让你的代码模块化,并保证其业务逻辑小而清晰。
模块是应用的组成部分,Riot.js 可以方便的构建和组织模块。
为什么要这么做?
无论对于你还是他人,小模块都是便于阅读、理解、维护、重用和调试的最佳选择。
如何做?
每个模块都需要符合 FIRST 原则:专注(Focused,单一职责)、独立(Independent)、可复用(Reusable)、简洁(Small)和可测试(Testable)。
如果你的模块功能太多,导致体积臃肿,请把它切分成多个小模块。比如确保每个模块的代码不超过 100 行,并使其与其它模块隔离。
小贴士
如果你使用 AMD 或 CommonJS 来加载模块,可以在命令行中加上--modular
参数
# enable AMD and CommonJS
riot --modular
模块的名字代表模块的用途,需要遵守以下要求:
为什么要这么做?
如何做?
<!-- 推荐 -->
<app-header />
<user-list />
<range-slider />
<!-- 避免 -->
<btn-group /> <!-- 虽然简短,但绕口,可以用 `button-group` 替代 -->
<ui-slider /> <!-- 所有的模块都是 UI 模块,所以 ui 在这里没有意义 -->
<slider /> <!-- 不符合 W3C 的自定义标签标准 -->
构建时把一个模块的所有文件打包成一个文件。
为什么这么做?
打包文件方便查找和重用。
如何做?
把模块名作为文件夹的名字和文件前缀,后缀为文件类型。
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
作为模块的文件名后缀,虽然它的本质是一个自定义标签。
为什么这么做?
如何做?
如果在浏览器端使用,可以这么写:
<script src="path/to/modules/my-example/my-example.tag.html" type="riot/tag"></script>
riot --ext tag.html modules/ dist/tags.js
如果你使用 Webpack tag loader,需要设置 loader:
{ test: /\.tag.html$/, loader: 'tag' }
<script>
标签Riot.js 支持在自定义标签里不使用 <script>
标签,但你需要把 Javascript 代码写在 <script>
标签里。这可以让代码更易于理解以及避免 IDE 识别混乱。
为什么这么做?
如何做?
<!-- 推荐 -->
<my-example>
<h1>The year is { this.year }</h1>
<script>
this.year = (new Date()).getUTCFullYear();
</script>
</my-example>
<!-- 避免 -->
<my-example>
<h1>The year is { this.year }</h1>
this.year = (new Date()).getUTCFullYear();
</my-example>
Riot.js 提供的标签内置语法支持 Javascript 语法,但这并不表示建议你在其中编写复杂的代码。
为什么这么做?
如何做?
把复杂的内置脚本移到模块变量或模块方法里。
<!-- 推荐 -->
<my-example>
{ year() + '-' + month() }
<script>
const twoDigits = (num) => ('0' + num).slice(-2);
this.month = () => twoDigits((new Date()).getUTCMonth() +1);
this.year = () => (new Date()).getUTCFullYear();
</script>
</my-example>
<!-- 避免 -->
<my-example>
{ (new Date()).getUTCFullYear() + '-' + ('0' + ((new Date()).getUTCMonth()+1)).slice(-2) }
</my-example>
Riot.js 支持通过自定义属性给模块添加参数,比如 <my-tag my-attr="{ value }" />
,可以在模块中通过 opts.MyAttr
获取参数。
尽管 Riot.js 支持使用复杂的原生 Javascript 语法来传递参数,但我们应保持参数的简洁,只使用标准的Javascript 类型,如字符串、数字、函数等。
允许的例外情况是只能使用复杂对象来传递参数(如一组对象、递归模块等)或通用的业务对象(如 Product
)。
为什么这么做?
如何做?
每个属性作为一个独立的参数,参数值为 Javascript 原生类型。
<!-- 推荐 -->
<range-slider
values="[10, 20]"
min="0"
max="100"
step="5"
on-slide="{ updateInputs }"
on-end="{ updateResults }"
/>
<!-- 避免 -->
<range-slider config="{ complexConfigObject }">
<!-- 例外: 迭代菜单模块 -->
<menu-item>
<a href="{ opts.url }">{ opts.text }</a>
<ul if="{ opts.items }">
<li each="{ item in opts.items }">
<menu-item
text="{ item.text }"
url="{ item.url }"
items="{ item.items }" />
</li>
</ul>
</menu-item>
在 Riot.js 中,参数是模块的 API。一个健壮的参数初始化过程,可以方便他人使用你的模块。
模块的参数可能是 Riot.js 的表达式(attr="{ var }"
)、纯字符串(attr="value"
)或不存在。你需要初始化处理这些情况。
为什么这么做?
初始化参数可以确保你的模块总是可以正常运行。哪怕其他开发者将其用于超出你预期的用途。
如何做?
在 Riot.js 的示例 中,可以把代码进行如下改进:
this.items = opts.items || []; // 设置默认值为空数组
这样改进后,可以让模块在以下情况中正常运作:
<todo items="{ [{ title:'Apples' }, { title:'Oranges', done:true }] }"> <!-- 传入参数时 -->
<todo> <!-- 未传入参数时 -->
对于 <range-slider>
这种模块,我们预期其参数都是数字类型,可以这么写:
// 如果没有传入 step,就设为默认值
this.step = !isNaN(Number(opts.step)) ? Number(opts.step) : 1;
以此确保以下情况都可以正常运行:
<range-slider> <!-- 全部使用默认值 -->
<range-slider step="5"> <!-- 将字符串 "5" 转变成数字 5 -->
<range-slider step="{ x }"> <!-- 使用变量 x -->
当 <range-slider>
模块支持可选的 on-slide
事件时,需要在使用前确认是否传入了回调:
slider.on('slide', (values, handle) => {
if (typeof opts.onSlide === 'function') {
opts.onSlide(values, handle);
}
}
以此确保以下情况可以正常运行:
<range-slider> <!-- 什么都不触发 -->
<range-slider on-slide="{ updateInputs }"> <!-- 触发 updateInputs -->
<range-slider on-slide="invalid option"> <!-- 什么都不触发 -->
在 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) {
/* ... */
}
你也可以把 mixins
和 observables
放在顶部:
/* recommended */
var tag = this;
// alphabetized properties
// alphabetized methods
tag.mixin('someBehaviour');
tag.on('mount', onMount);
tag.on('update', onUpdate);
// etc
Riot.js 支持模仿 ES6 的方法声明语法,把 methodName() { }
编译成 this.methodName = function() {}.bind(this)
。但这并不是 ES6 的标准语法。
为什么这么做?
如何做?
使用 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 原则,应当避免使用。
例外的情况是在循环中,且子对象是匿名模块。
为什么这么做?
如何做?
<!-- 推荐 -->
<parent-tag>
<child-tag value="{ value }" /> <!-- 把值传入子模块 -->
</parent-tag>
<child-tag>
<span>{ opts.value }</span> <!-- 使用父模块传入的值 -->
</child-tag>
<!-- 避免 -->
<parent-tag>
<child-tag />
</parent-tag>
<child-tag>
<span>value: { parent.value }</span> <!-- 不要这么做 -->
</child-tag>
<!-- 推荐 -->
<parent-tag>
<child-tag on-event="{ methodToCallOnEvent }" /> <!-- 传入回调函数 -->
<script>this.methodToCallOnEvent = () => { /*...*/ };</script>
<parent-tag>
<child-tag>
<button onclick="{ opts.onEvent }"></button> <!-- 事件触发时通知父模块 -->
</child-tag>
<!-- 避免 -->
<parent-tag>
<child-tag />
<script>this.methodToCallOnEvent = () => { /*...*/ };</script>
<parent-tag>
<child-tag>
<button onclick="{ parent.methodToCallOnEvent }"></button> <!-- 不要这么做 -->
</child-tag>
<!-- 允许的例外情况 -->
<parent-tag>
<button each="{ item in items }"
onclick="{ parent.onEvent }"> <!-- button 不是 Riot.js 的模块 -->
{ item.text }
</button>
<script>this.onEvent = (e) => { alert(e.item.text); }</script>
</parent-tag>
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 }"
。
<!-- 推荐 -->
<ul>
<li each="{ item in items }">
<label class="{ completed: item.done }">
<input type="checkbox" checked="{ item.done }"> { item.title }
</label>
</li>
</ul>
<!-- 推荐 -->
<ul>
<li each="{ key, item in items }">
<label class="{ completed: item.done }">
<input type="checkbox" checked="{ item.done }"> { key }. { item.title }
</label>
</li>
</ul>
<!-- 避免 -->
<ul>
<li each="{ items }">
<label class="{ completed: done }">
<input type="checkbox" checked="{ done }"> { title }
</label>
</li>
</ul>
为了给开发者提供便利,Riot.js 允许在模块中嵌套 <style>
,并提供了 scope 功能来限制样式的作用域,但这并不是真正的样式隔离。
为什么这么做?
如何做?
将样式拆分为独立文件放在模块文件夹内:
my-example/
├── my-example.tag.html
├── my-example.(css|less|scss) <-- external stylesheet next to tag file
└── ...
Riot.js 的模块是一个自定义标签,标签名非常适合作为样式的作用域。
为什么这么做?
如何做?
使用标签名作为样式的父类或作用域。
/* 推荐 */
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 */
小贴士
如果使用 2.3.17 之后的版本,可以使用 [data-is="my-example"]
来替代 .my-example
一个模块包括了属性和方法,为了便于其他开发人员使用,需要写一份文档来说明这些属性和方法。
为什么这么做?
README.md
是标准的说明文件格式,像 Github 之类的代码托管工具,可以方便的展示和阅读这份文件。如何做?
添加 README.md
到模块文件夹:
range-slider/
├── range-slider.tag.html
├── range-slider.less
└── README.md
在文档中,描述该模块的功能和使用方法,并说明其自定义属性和方法的含义和用法。
# 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>` 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).
增加 *.demo.html
示例文件,来表示模块该如何被使用。
为什么这么做?
如何做?
添加 *.demo.html
文件到模块文件夹:
city-map/
├── city-map.tag.html
├── city-map.demo.html
├── city-map.css
└── ...
在示例文件中,需要:
riot+compiler.min.js
来解析和执行示例;./city-map.tag.html
;demo
标签用于嵌入模块;<demo>
中编写示例;demo
标签加上 aria-label
属性来说明示例的内容;riot.mount('demo', {})
来初始化。下面是一个例子:
<!-- 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;
}
</style>
</body>
检查工具可以改进代码风格的一致性并找出语法错误。通过一些额外的配置,我们可以检查 Riot.js 模块的代码风格。
为什么这么做?
如何做?
RiotJS Style Guide
标识给项目加上标识,并链接到本指南。
为什么这么做?
让其他开发人员知道和了解本指南。
怎么做?
在 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 文件中引入:
<a href="https://github.com/voorhoede/riotjs-style-guide">
<img alt="RiotJS Style Guide badge"
src="https://cdn.rawgit.com/voorhoede/riotjs-style-guide/master/riotjs-style-guide.svg">
</a>