其他技巧
提取组件中的业务逻辑
编写
class Money extends Component {
static propTypes = {
currency: PropTypes.string.isRequired,
amount: PropTypes.number.isRequired
};
getCurrencyData() {
return {
CNY: { base: 100, symbol: "¥" }
// ...
}[this.props.currency];
}
formatAmount(amount, base) {
return parseFloat(amount / base).toFixed(2);
}
render() {
const currency = this.getCurrencyData();
// ...
}
}
组件本身只是用来进行界面渲染与交互处理的,而在上面的
export const getCurrencyData = currency => {
return {
// ...
}[currency];
};
export const formatAmount = (amount, base) => {
return parseFloat(amount / base).toFixed(2);
};
然后可以编写针对这两个函数的测试用例:
test("it formats the amount to 2 dp", () => {
expect(formatAmount(2000, 100)).toEqual("20.00");
});
test("respects the base", () => {
expect(formatAmount(2000, 10)).toEqual("200.00");
});
test("it deals with decimal places correctly", () => {
expect(formatAmount(2050, 100)).toEqual("20.50");
});
test("for GBP it returns the right data", () => {
expect(getCurrencyData("GBP")).toEqual({
base: 100,
symbol: "£"
});
});
此时render
方法,我们自然可以将该组件重构为无状态函数式组件,在进行逻辑分割的同时提升了组件性能:
const Money = ({ currency, amount }) => {
const currencyData = getCurrencyData(currency);
// ...
};
Money.propTypes = {
currency: PropTypes.string.isRequired,
amount: PropTypes.number.isRequired
};
软件开发的工程本身就是不断地重构,通过将组件中的业务逻辑处理流程提取为独立函数,我们不仅简化了组件本身的代码,还同时提升了测试覆盖率与组件的性能。在;而在代码可读性提升的同时,我们也更容易发现
重构冗余代码
在开发的过程中我们往往会存在大量的复制粘贴代码的行为,这一点在项目的开发初期尤其显著;而在项目逐步稳定,功能需求逐步完善之后我们就需要考虑对代码库的优化与重构,尽量编写清晰可维护的代码。好的代码往往是在合理范围内尽可能地避免重复代码,遵循单一职责与
当然,我们并不是单纯地追求公共代码地完全剥离化,过度的抽象反而会降低代码的可读性与可理解性。npm
全局安装 jsinspect
命令:
Usage: jsinspect [options] <paths ...>
Detect copy-pasted and structurally similar JavaScript code
Example use: jsinspect -I -L -t 20 --ignore "test" ./path/to/src
Options:
-h, --help output usage information
-V, --versionoutput the version number
-t, --threshold <number> number of nodes (default: 30)
-m, --min-instances <number> min instances for a match (default: 2)
-c, --config path to config file (default: .jsinspectrc)
-r, --reporter [default|json|pmd]specify the reporter to use
-I, --no-identifiers do not match identifiers
-L, --no-literalsdo not match literals
-C, --no-color disable colors
--ignore <pattern> ignore paths matching a regex
--truncate <number>length to truncate lines (default: 100, off: 0)
我们也可以选择在项目目录下添加 .jsinspect
配置文件指明
{
"threshold": 30,
"identifiers": true,
"literals": true,
"ignore": "test|spec|mock",
"reporter": "json",
"truncate": 100
}
在配置完毕之后,我们可以使用 jsinspect -t 50 --ignore "test" ./path/to/src
来对于代码库进行分析,以笔者找到的某个代码库为例,其检测出了上百个重复的代码片,其中典型的代表如下所示。可以看到在某个组件中重复编写了多次密码输入的元素,我们可以选择将其封装为函数式组件,将 label
、hintText
等通用属性包裹在内,从而减少代码的重复率。
Match - 2 instances
./src/view/main/component/tabs/account/operation/login/forget_password.js:96,110
return <div className="my_register__register">
<div className="item">
<Paper zDepth={2}>
<EnhancedTextFieldWithLabel
label="密码"
hintText="请输入密码,6-20位字母,数字"
onChange={(event, value)=> {
this.setState({
userPwd: value
})
}}
/>
</Paper>
</div>
<div className="item">
./src/view/main/component/tabs/my/login/forget_password.js:111,125
return <div className="my_register__register">
<div className="item">
<Paper zDepth={2}>
<EnhancedTextFieldWithLabel
label="密码"
hintText="请输入密码,6-20位字母,数字"
onChange={(event, value)=> {
this.setState({
userPwd: value
})
}}
/>
</Paper>
</div>
<div className="item">
笔者也对于
// ./src/renderers/dom/fiber/wrappers/ReactDOMFiberTextarea.js:134,153
var value = props.value;
if (value != null) {
// Cast `value` to a string to ensure the value is set correctly. While
// browsers typically do this as necessary, jsdom doesn't.
var newValue = '' + value;
// To avoid side effects (such as losing text selection), only set value if changed
if (newValue !== node.value) {
node.value = newValue;
}
if (props.defaultValue == null) {
node.defaultValue = newValue;
}
}
if (props.defaultValue != null) {
node.defaultValue = props.defaultValue;
}
},
postMountWrapper: function(element: Element, props: Object) {
// ./src/renderers/dom/stack/client/wrappers/ReactDOMTextarea.js:129,148
var value = props.value;
if (value != null) {
// Cast `value` to a string to ensure the value is set correctly. While
// browsers typically do this as necessary, jsdom doesn't.
var newValue = '' + value;
// To avoid side effects (such as losing text selection), only set value if changed
if (newValue !== node.value) {
node.value = newValue;
}
if (props.defaultValue == null) {
node.defaultValue = newValue;
}
}
if (props.defaultValue != null) {
node.defaultValue = props.defaultValue;
}
},
postMountWrapper: function(inst) {
笔者认为在新特性的开发过程中我们不一定需要时刻地考虑代码重构,而是应该相对独立地开发新功能。最后我们再简单地讨论下inspector.js
文件中:
// ...
this._filePaths.forEach(filePath => {
var src = fs.readFileSync(filePath, { encoding: "utf8" });
this._fileContents[filePath] = src.split("\n");
var syntaxTree = parse(src, filePath);
this._traversals[filePath] = nodeUtils.getDFSTraversal(syntaxTree);
this._walk(syntaxTree, nodes => this._insert(nodes));
});
this._analyze();
// ...
上述流程还是较为清晰的,
Node {
type: 'Program',
start: 0,
end: 31,
loc:
SourceLocation {
start: Position { line: 1, column: 0 },
end: Position { line: 2, column: 15 },
filename: './__test__/a.js' },
sourceType: 'script',
body:
[ Node {
type: 'ExpressionStatement',
start: 0,
end: 15,
loc: [Object],
expression: [Object] },
Node {
type: 'ExpressionStatement',
start: 16,
end: 31,
loc: [Object],
expression: [Object] } ],
directives: [] }
{ './__test__/a.js': [ 'console.log(a);', 'console.log(b);' ] }
其后我们通过深度优先遍历算法在-t
参数就是用来指定分割的原子比较对象的维度,当我们将该参数指定为_map
结构如下:
{
"uj3VAExwF5Avx0SGBDFu8beU+Lk=": [[[Object], [Object]], [[Object], [Object]]],
"eMqg1hUXEFYNbKkbsd2QWECLiYU=": [[[Object], [Object]], [[Object], [Object]]],
"gvSCaZfmhte6tfnpfmnTeH+eylw=": [[[Object], [Object]], [[Object], [Object]]],
"eHqT9EuPomhWLlo9nwU0DWOkcXk=": [[[Object], [Object]], [[Object], [Object]]]
}
如果有大规模代码数据的话我们可能形成很多有重叠的实例,这里使用了 _omitOverlappingInstances
函数来进行去重;譬如如果某个实例包含节点
_prune(nodeArrays) {
for (let i = 0; i < nodeArrays.length; i++) {
let nodes = nodeArrays[i];
for (let j = 0; j < nodes.length; j++) {
this._removeNode(nodes[j]);
}
}
}
避免深层嵌套
- 2017-Avoiding deeply nested component trees: By passing child components down instead of data you can avoid passing data down through many levels of components.
随着代码库中组件数量与对应的
class TodosListPage extends Component {
componentDidMount() {
this.props.fetchTodos();
}
render() {
const {
todos,
newTodoText,
addTodo,
filterText,
setFilterText
} = this.props;
return (
<div>
<AddTodoForm newTodoText={newTodoText} addTodo={addTodo}/>
<TodosFilter filterText={filterText}
setFilterText={setFilterText}/>
<TodoList todos={todos}
</div>
);
}
}
上述代码中propTypes
类型,示例代码如下:
class TodosListPage extends Component {
render() {
const { addTodoForm, todosList, todosFilter } = this.props;
return (
<div>
<AddTodoForm {...addTodoForm} />
<TodosFilter {...todosFilter} />
<TodoList {...todosList} />
</div>
);
}
componentDidMount() {
this.props.fetchTodos();
}
}