系列文章
本系列文章已收录到专栏,交流群号:689220994,也可点击链接加入。
前言
在开发创建一个新项目的时候,我们一般都会使用平台自带的脚手架,如下图所示:
或者是使用网页版:
尽管平台已经提供了灵活的配置项,甚至是可以修改原有的模板内容,例如创建插件的plugin.xml
的模板:
但是我们并没有办法增删脚手架所创建的内容,也没有办法在模板中使用未提供的参数。因此,本文将介绍如何创建自己的脚手架,不过本文所介绍的方法只适用于 IDEA,对于其它平台(WebStorm、PyCharm)将在后续文章进行介绍,本文所涉及到的完整代码也已上传到GitHub。
最终效果
实现思路
- 继承
ModuleBuilder
实现自己的模块创建方式。 - 自定义模块类型、设置面板和模板文件。
- 读取自定义内容创建项目目录和文件。
注册并实现自己的模块构建类
首先创建TemplateBuilder
类继承ModuleBuilder
类(这里先展示全部内容,后续会针对其中的方法按点进行介绍):
class TemplateBuilder: ModuleBuilder() {
// 1. 持久化配置
private val state = TemplateState.getInstance()
// 2. 模块类型
override fun getModuleType() = TemplateModuleType()
// 3. 项目创建配置
override fun setupRootModel(rootModel: ModifiableRootModel) {
// 3.1 设置项目路径
val virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByPath(state.location)
rootModel.addContentEntry(virtualFile!!)
// 3.2 读取模板并设置变量
val template = FileTemplateManager.getInstance(rootModel.project).getJ2eeTemplate(TemplateFileFactory.PLUGIN_XML)
val properties = Properties()
properties.setProperty("name", state.name)
properties.setProperty("group", state.group)
properties.setProperty("artifact", state.artifact)
properties.setProperty("location", state.location)
properties.setProperty("description", state.description)
properties.setProperty("author", state.author)
properties.setProperty("email", state.email)
properties.setProperty("blogUrl", state.blogUrl)
val renderedText = template.getText(properties)
// 3.3 创建 resources 文件夹并写入 XML 文件
val resFile = File("${state.location}${File.separator}src${File.separator}resources")
resFile.mkdirs()
val file = File(resFile.absolutePath + File.separator + TemplateFileFactory.PLUGIN_XML)
FileUtil.writeToFile(file, renderedText)
// 3.4 设置 source 文件夹
rootModel.contentEntries.forEach {
val src = LocalFileSystem.getInstance().refreshAndFindFileByPath("${state.location}${File.separator}src")!!
it.addSourceFolder(src, false)
}
}
// 4. 设置项目名称
override fun modifyProjectTypeStep(settingsStep: SettingsStep): ModuleWizardStep? {
settingsStep.context.projectName = state.name
return super.modifyProjectTypeStep(settingsStep)
}
// 5. 创建项目
override fun createProject(name: String?, path: String?) = super.createProject(state.name, state.location)
// 6. 设置第二个步骤
override fun createWizardSteps(wizardContext: WizardContext, modulesProvider: ModulesProvider) =
arrayOf(TemplateSecondStep())
// 7. 设置第一个步骤
override fun getCustomOptionsStep(context: WizardContext?, parentDisposable: Disposable?) = TemplateFirstStep()
// 8. 忽略自带的步骤
override fun getIgnoredSteps(): MutableList<Class<out ModuleWizardStep>> = mutableListOf(ProjectSettingsStep::class.java)
}
然后在plugin.xml
中进行配置:
<extensions defaultExtensionNs="com.intellij">
<moduleBuilder builderClass="cn.butterfly.template.module.TemplateBuilder"/>
</extensions>
自定义模块类型
TemplateBuilder
中的第二点的getModuleType()
返回了TemplateModuleType()
,其中TemplateModuleType
就是我们自定义的模块类型,可以用于设置模块的图标,名称以及描述:
class TemplateModuleType(id: @NonNls String = "TEMPLATE_MODULE") : ModuleType<TemplateBuilder>(id) {
override fun createModuleBuilder() = TemplateBuilder()
// 设置模块名称
override fun getName() = "Template"
// 设置描述
override fun getDescription() = "TEMPLATE_MODULE"
// 设置模块图标
override fun getNodeIcon(isOpened: Boolean) = PluginIcons.TEMPLATE_ICON
}
对应效果如下:
自定义设置面板
TemplateBuilder
中的第六和七点对应我们自己的设置面板,其中第七点对应下图中的效果:
对应的代码如下(这里只展示第七点即第一个设置面板内容,第二个界面的代码类似,在GitHub上进行查看即可),需要继承并实现ModuleWizardStep
作为脚手架中的一个设置步骤:
class TemplateFirstStep: ModuleWizardStep() {
private val model = Model()
// 持久化配置
private val state: TemplateState = TemplateState.getInstance()
// UI 界面
private var panel = panel {
indent {
row("Name: ") {
textField().bindText(model::name)
}.topGap(TopGap.MEDIUM)
row("Location: ") {
textFieldWithBrowseButton(
"",
ProjectManager.getInstance().defaultProject,
FileChooserDescriptorFactory.createSingleFolderDescriptor()
).bindText(model::location)
}.topGap(TopGap.MEDIUM)
row("Group: ") {
textField().bindText(model::group)
}.topGap(TopGap.MEDIUM)
row("Artifact: ") {
textField().bindText(model::artifact)
}.topGap(TopGap.MEDIUM)
row("Description: ") {
textField().bindText(model::description)
}.topGap(TopGap.MEDIUM)
}
}
override fun getComponent() = panel
// 更新数据
override fun updateDataModel() {
panel.apply()
state.name = model.name
state.group = model.group
state.artifact = model.artifact
state.location = model.location
state.description = model.description
}
// 数据存储类
data class Model(
var name: String = "",
var group: String = "",
var artifact: String = "",
var location: String = "",
var description: String = ""
)
}
这里使用了Kotlin UI DSL
来绘制 UI,代码比较简单,不再介绍。其中持久化配置(TemplateBuilder
中的第一点也使用了该持久化配置)对应的代码如下:
@Service
@State(name = "TemplateState", storages = [Storage("template-state.xml")])
class TemplateState: PersistentStateComponent<TemplateState> {
var name = ""
var group = ""
var artifact = ""
var location = ""
var description = ""
var email = "1945912314@qq.com"
var author = "butterfly"
var blogUrl = "https://juejin.cn/user/3350967174567352"
override fun getState() = this
override fun loadState(state: TemplateState) {
XmlSerializerUtil.copyBean(state, this)
}
companion object {
fun getInstance(): TemplateState = ApplicationManager.getApplication().getService(TemplateState::class.java)
}
}
需要在plugin.xml
中进行配置:
<extensions defaultExtensionNs="com.intellij">
<applicationService serviceImplementation="cn.butterfly.template.state.TemplateState"/>
</extensions>
自定义模板文件
为了方便读取和后期的可定制性,本文也参考其它脚手架项目的思路将模板文件添加到了 IDEA 自带的模板管理中(这里只介绍如何添加到模板的Other模块,详细内容可参考官网),首先在 resources 文件夹下创建fileTemplates/j2ee
文件层级,然后在下面创建一个template-plugin.xml.ft
(在原有文件名基础上以.ft结尾)文件用于保存模板内容,之后再创建一个TemplateFileFactory
类实现FileTemplateGroupDescriptorFactory
接口,用于将模板文件注册到 IDEA 中:
class TemplateFileFactory: FileTemplateGroupDescriptorFactory {
companion object {
// 上述创建模板文件去掉.ft 后缀
const val PLUGIN_XML = "template-plugin.xml"
}
override fun getFileTemplatesDescriptor(): FileTemplateGroupDescriptor {
// 设置模板分组
val templateGroup = FileTemplateGroupDescriptor("Template", PluginIcons.TEMPLATE_ICON)
// 添加模板文件
templateGroup.addTemplate(FileTemplateDescriptor(PLUGIN_XML, PluginIcons.TEMPLATE_ICON))
return templateGroup
}
}
然后在plugin.xml
中进行配置:
<extensions defaultExtensionNs="com.intellij">
<fileTemplateGroup implementation="cn.butterfly.template.template.TemplateFileFactory"/>
</extensions>
然后就可以在 IDEA 中进行管理了:
创建项目结构和文件
TemplateBuilder
中的第三点即用于读取上述中的模板和配置然后用于生成项目文件,其中 3.1 用于读取第一步骤中的 location 值然后作为项目路径,3.2 即是读取上文中模板然后再根据读取到的配置完善文件内容,3.3 用于创建目录层级并将生成的文件写入到 resources 文件夹中,3.4 步骤则是手动将文件夹标记为源代码文件夹
其它细节
TemplateBuilder
中的第四和五点用于手动设置项目的名称和项目路径。
TemplateBuilder
中的第八点忽略步骤用于忽略掉平台自带的创建项目步骤,如果不进行忽略,就会在原有的步骤基础上多一个步骤:
总结
本文简单介绍了如何在 IDEA 中创建属于自己的脚手架,关于自动识别模块(可在创建完项目后手动导入解决)和其它平台的脚手架创建方式将在后续文章中进行介绍,大家也可以加入开头提到的交流群一起交流讨论。