本文介绍一种自动生成脚手架代码的方法,基于Swift代码做示例,方法适用于其它编程语言。
项目开发的趋势之一就是进行功能职责清晰的代码分层,譬如卡搭编程iOS端按照MVVM结构进行代码结构划分后,View层和ViewModel可以更好复用,Controller层代码得到精简,针对ViewModel层也能够便捷进行单元测试了。
分层带来以上看得见的好处的同时,也带来一个不可回避的影响,就是开发速度的降低 —— 每增加一个分层,意味着要多写一层的代码,以及与这层代码交互的其它代码。
复用性和开发效率之间也需要一定的平衡。
分层方案、协议、命名和代码规范固定之后,可以发现要多写的代码是存在一定共性的,这些代码除了类名和少数变化外结构非常相似,却又较难进一步抽取(比如实现UITableViewDelegate和UITableViewDataSource的那一堆代码,即便抽取成一些配置方法,每次还是需要大段代码来调用这些配置方法),那为什么不考虑通过工具自动生成这些“脚手架”代码呢?
借助接口管理平台NEI提供的nei-toolkit这个工具,卡搭编程iOS端现在完全通过NEI上录入的网络请求接口和数据模型信息,来生成Swift的网络请求Service层和纯数据Model层代码,受这个工具启发,我编写了个小工具进一步生成Controller、View、ViewModel层里的框架性代码。
自动生成的代码包括:
代码生成工具通过简单配置和一行命令./generate.js
,几秒钟就能生成展示如下一个基于UITableView的界面所需的全部代码:
该界面包含功能有:
具体使用时,填充如下配置,就能生成下面的文件夹结构和相应代码(共200多行代码):
var configs = {
"Module": "DownloadManager",
"author": "Wang Jiawen"
}
自动生成代码一般有两种思路:
第1种方式的优点是基本不用处理代码缩进等问题,效率高,适合处理生成代码较多的情况; 第2种方式则更加灵活,可精细化输出有一定逻辑的代码,但是代码缩进空格的输出较难控制。
如果生成的代码有一定复杂度,则两种方法会同时用到。本文使用Handlebars这一模板引擎来实现方法1的代码替换,使用Node.js处理方法2的字符串拼接、文件操作等逻辑。
Handlebars 是 JavaScript 一个语义模板库,通过对view和data的分离来快速构建Web模板。一般用在前端开发中的页面生成,能够生成网页也就能用于生成其它文本格式的代码。
本工具通过npm进行包管理,执行命令npm install --save handlebars
安装包,js文件中通过require('handlebars')
进行库引入。
expressions标签 是Handlebars中的基本元素,分为两类标签:
以两对花括号包裹标签名{{value}}
方式表示,Handlebars能自动匹配其中的value为具体数值或函数。
当需要在模板文件中进行条件判断、循环等逻辑操作时,需要用到以{{#blockName}}
开头{{/blockName}}
结尾的表达式标签。
其中的blockName可以是:
表达式标签在生成复杂代码时非常有用。更多Handlebars简介、基本用法可参考《Handlebars.js 模板引擎》和官网。
比如上文提到的configs.js文件中有如下配置:
// configs.js
var configs = {
"Module": "DownloadManager",
"author": "Wang Jiawen"
}
module.exports = configs;
在{{Module}}TableViewCell.swift模板文件中有如下内容:
// {{Module}}TableViewCell.swift
//
// Created by {{author}} on {{date}}.
//
// Copyright © 2018年 NetEase. All rights reserved.
import UIKit
class {{Module}}TableViewCell: UITableViewCell {
@IBOutlet weak var mainImageView: UIImageView!
@IBOutlet weak var titleLabel: UILabel!
}
extension {{Module}}TableViewCell: TableViewCellProtocol {
func bindWithViewModel(_ viewModel: TableViewCellViewModelProtocol?) {
guard let viewModel = viewModel as? {{Module}}CellViewModel else {
return
}
titleLabel.text = viewModel.title
mainImageView.setWebImageWith(viewModel.imageUrl, placehold: nil)
}
}
运行命令npm install --save handlebars
安装格式化时间的依赖库moment,编写以下生成代码的脚本generate.js,
// generate.js
#!/usr/bin/env node
var config = require("./config");
var handlebars = require('handlebars');
var fs = require('fs');
var moment = require("moment")
// 向handlebars中注册{{date}}函数调用标签
handlebars.registerHelper('date', function (items, options) {
var time = moment().format("YYYY/M/d")
return new handlebars.SafeString(time)
});
// 读取模板
var source = fs.readFileSync("{{Module}}TableViewCell.swift", "utf8");
// 预编译模板内容
var template = handlebars.compile(source);
// 匹配模板文件和配置文本内容,输出编译后的内容
var result = template(config)
console.log(result)
为generate.js添加执行权限chmod 755 generate.js
,控制台直接运行./generate.js
后可看到输出的文本为:
// DownloadManagerTableViewCell.swift
//
// Created by Wang Jiawen on 2018/7/5.
//
// Copyright © 2018年 NetEase. All rights reserved.
import UIKit
class DownloadManagerTableViewCell: UITableViewCell {
@IBOutlet weak var mainImageView: UIImageView!
@IBOutlet weak var titleLabel: UILabel!
}
extension DownloadManagerTableViewCell: TableViewCellProtocol {
func bindWithViewModel(_ viewModel: TableViewCellViewModelProtocol?) {
guard let viewModel = viewModel as? DownloadManagerCellViewModel else {
return
}
titleLabel.text = viewModel.title
mainImageView.setWebImageWith(viewModel.imageUrl, placehold: nil)
}
}
上述示例就包含了最基础的标签{{Module}}、{{author}}文本替换,自定义JS函数提供{{date}}标签内容。
npm init -y
npm install handlebars --save
npm install moment --save
npm install shelljs --save
则此时ModuleGenerate结构为:
- ModuleGenerate
- config.js
- generate.js
- package.json
- node_modules
- templates
- {{Module}}
- {{Module}}.storyboard]
- Controller
- {{Module}}ViewController.swift
- View
- {{TableCell}}TableViewCell.swift
- {{TableCell}}TableViewCell.xib
- ViewModel
- {{Module}}ViewModel.swift
- {{TableCell}}CellViewModel.swift
编写generate.js文件:
// 遍历处理dir下的文件
function handleFile(dir) {
fs.readdir(dir, function (err, files) {
if (err) {
console.warn(err)
} else {
files.forEach(function (fileName) {
var fileDir = path.join(dir, fileName)
fs.stat(fileDir, function (err, stats) {
if (err) {
console.warn("get stats fail" + err)
} else {
if (stats.isFile()) {
if (!fileName.startsWith(".")) {
console.log("处理:" + fileDir)
handleTemplate(fileDir)
}
} else if (stats.isDirectory()) {
handleFile(fileDir)
}
}
})
})
}
})
}
// 生成源码,传入templatefile为模板文件路径
function handleTemplate(templatefile) {
var source = fs.readFileSync(templatefile, "utf8");
var template = handlebars.compile(source);
var result = template(config)
// 将路径中的标签替换为配置中的变量值作为保存路径
var saveFilePath = templatefile.replace(new RegExp(templateDir, 'g'), config.Module).replace(/{{Module}}/g, config.Module)
var filePath = path.join(targetDir, saveFilePath)
console.log("生成:" + filePath)
// 文件不存在则创建
ensureDirExists(filePath)
fs.writeFile(filePath, result, function (e) {
if (e) {
console.log("error: " + s.name + " : " + e);
}
});
}
以上即是工具编写的流程,此时工具功能还比较简单,只相当于IDE的Replace All和文件重命名功能,借助Handlebars和node.js可实现更多定制化需求。
生成的目标代码大而全,大多数情况下并不是每段代码都需要,那可以针对代码块做些配置,以决定是否生成某些部分代码。
例如,config.js中会出现以下配置:
var configs = {
"needRouter": true // 是否需要实现路由协议
}
模板代码中,使用{{#if condition}}{{/if}}标签对,仅当needRouter配置为true时才生成标签对包裹的代码:
{{#if needRouter}}
// MARK: - Routable
extension {{Module}}ViewController: Routable {
static var routePath: Router.Page {
return .{{Module}}ViewController
}
static func routePageCreate(parameters: [String: Any]?, object: Any?) -> UIViewController? {
let vc = {{Module}}ViewController.createInstance()
return vc
}
}
{{/if}}
上述示例生成的TableView只有一个Cell且命名固定,如果支持多个Cell,则能在代码生成时一次性生成更多Cell相关文件(一个Cell对应3个文件),并在使用到Cell的地方填充相关的测试数据。
config.js中可以如下配置:
var configs = {
"tableViewCells": [ // 生成UITableViewCell及其ViewModel的名称前缀
"DownloadManager",
"DownloadManagerDownloading",
"EmptyContent"
]
}
模板代码中,使用到cell的地方以数组标签方式输出代码,类似:
{{#tableViewCells}}
tableView.register(UINib(nibName: {{this}}CellViewModel.identifier, bundle: nil),
forCellReuseIdentifier: {{this}}CellViewModel.identifier)
{{/tableViewCells}}
generate.js中需要为Cell模板的遍历读取和生成逻辑进行修改,此时逻辑变为先用sed命令将模板文件中的{{TableCell}}标签替换为tableViewCells中的Cell名,再把替换好后的文本作为模板使用,主要代码:
// 生成Cell相关的代码
config.tableViewCells.forEach(function (cell) {
console.log(cell)
cellTeplateFilePaths.forEach(function (filePath) {
console.log("Path:" + filePath)
handleCellTemplate(filePath, cell)
})
})
// 传入模板名和生成的Cell名,生成与Cell相关的几个文件
function handleCellTemplate(templatefile, cellName) {
var source = shell.exec('sed -e "s/{{TableCell}}/' + cellName + '/g" ' + templatefile).stdout
var template = handlebars.compile(source);
var result = template(config)
var saveFilePath = templatefile.replace(new RegExp(templateDir, 'g'), config.Module)
.replace(/{{Module}}/g, config.Module)
.replace(/{{TableCell}}/g, cellName)
var filePath = path.join(targetDir, saveFilePath)
console.log("生成:" + filePath)
ensureDirExists(filePath)
fs.writeFile(filePath, result, function (e) {
if (e) {
console.log("error: " + s.name + " : " + e);
}
});
}
除了本文示例,相信该方法可以用到更多能够解放生产力的地方。
本文来自网易实践者社区,经作者王佳文授权发布。