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();
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) {
/* ... */
}
你也可以把 mixins 和 observables 放在顶部:
/* 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 模块的代码风格。
为什么这么做?
- 保证开发者们有一致的代码风格;
- 提前发现语法错误。
如何做?
给你的项目加上 RiotJS Style Guide 标识
给项目加上标识,并链接到本指南。
为什么这么做?
让其他开发人员知道和了解本指南。
怎么做?
在 markdown 文件中引入:
[](https://github.com/voorhoede/riotjs-style-guide)
在 html 文件中引入: