使用 Cookiecutter 创建 Phoenix 项目模板
虽然 Phoenix 提供了 mix phx.new 命令快速创建项目,但团队协作或个人习惯往往需要统一的代码规范、预配置的依赖项和标准化的项目结构。
本文将介绍如何基于 Cookiecutter 打造一个可复用的 Phoenix 项目模板。
环境要求
| 工具 | 版本要求 |
|---|---|
| Cookiecutter | 2.6+ |
| Elixir | 1.18+ |
| Erlang | 27.2+ |
| Phoenix | 1.17+ |
| Python | 3.10+ |
安装方式:
# macOS
brew install cookiecutter
# Linux
pip install --user cookiecutter
模板初始化
创建项目结构
mkdir cookiecutter-phoenix && cd cookiecutter-phoenix
配置文件
cookiecutter.json 定义了模板的变量:
{
"app_name": "my_app",
"app_module": "{{ cookiecutter.app_name | replace('_', ' ') | title | replace(' ', '') }}",
"author_name": "Sean Gong",
"app_version": "0.1.0",
"elixir_version": "1.18.2-otp-27",
"erlang_version": "27.2.4"
}
生成基础项目
mix phx.new awesome_app --no-html \
--no-assets \
--no-gettext \
--no-dashboard \
--no-live \
--no-mailer
# 版本管理文件
cat <<EOF > awesome_app/.tool-versions
erlang {{cookiecutter.erlang_version}}
elixir {{cookiecutter.elixir_version}}
EOF
模板定制
核心策略
| 策略 | 说明 |
|---|---|
| 文件/文件夹重命名 | 将 awesome_app 替换为用户输入的项目名 |
| 内容替换 | 使用正则表达式替换模块名、配置项等 |
| 模板语法转义 | 避免 Jinja2 误解析 Elixir 代码 |
批量替换脚本
# 1. 重命名文件和文件夹
find ./awesome_app -depth -name '*awesome_app*' -execdir sh -c 'mv "$1" "$(echo "$1" | sed "s/awesome_app/{{cookiecutter.app_name}}/g")"' _ {} \;
# 2. 替换文件中的模块名
find ./ -type f -exec perl -pi -e 's/AwesomeAppWeb/{{ cookiecutter.app_module }}Web/g' {} +
find ./ -type f -exec perl -pi -e 's/AwesomeApp/{{ cookiecutter.app_module }}/g' {} +
# 3. 检查是否有遗漏(使用 VSCode 搜索替换功能)
模板语法转义
单行转义:
mod: {{'{'}}{{ cookiecutter.app_module }}.Application, []{{'}'}}
多行转义:
{% raw -%}
{:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
{% endraw -%}
扩展开发
注册自定义函数
通过 Jinja2 扩展,可以添加自定义全局函数和过滤器:
# local_extensions.py
#!/usr/bin/env python3
from jinja2.ext import Extension
import secrets
import urllib.request
import json
deps_default_versions = {
'oban': "2.19",
}
def latest_package_version(name: str) -> str:
"""从 hex.pm 获取指定包的最新版本号(主次版本)"""
try:
with urllib.request.urlopen(f"https://hex.pm/api/packages/{name}") as response:
data = json.loads(response.read().decode())
version = data['releases'][0]['version']
major_minor = '.'.join(version.split('.')[:2])
return f"~> {major_minor}"
except Exception as e:
print(f"获取 {name} 版本失败: {e}")
return deps_default_versions[name]
class CustomExtension(Extension):
def __init__(self, environment):
super().__init__(environment)
# 生成安全密钥
environment.globals['gen_secret'] = lambda v: secrets.token_urlsafe(v)
# 获取最新版本
environment.globals['latest_version'] = latest_package_version
启用扩展
在 cookiecutter.json 中添加:
{
"app_name": "my_app",
"_extensions": ["local_extensions.CustomExtension"]
}
使用示例
# config/dev.exs
config :{{ cookiecutter.app_name }}, {{ cookiecutter.app_module }}Web.Endpoint,
http: [ip: {127, 0, 0, 1}, port: 4000],
check_origin: false,
code_reloader: true,
debug_errors: true,
secret_key_base: "{{ gen_secret(48) }}",
watchers: []
可选模块:添加 Oban
以添加任务队列 Oban 为例:
1. 添加配置变量
{
"use_oban": "y"
}
2. 条件依赖
# mix.exs
defp deps do
[
{% if cookiecutter.use_oban == 'y' -%}
{:oban, "{{ latest_version('oban') }}"},
{% endif -%}
]
end
3. 条件配置
# config/config.exs
{% if cookiecutter.use_oban == 'y' -%}
config :{{ cookiecutter.app_name }}, Oban,
engine: Oban.Engines.Basic,
queues: [default: 10],
repo: {{ cookiecutter.app_module }}.Repo,
prefix: "oban"
{% endif -%}
# config/test.exs
{% if cookiecutter.use_oban == 'y' -%}
config :{{ cookiecutter.app_name }}, Oban, testing: :manual
{% endif -%}
# lib/{{cookiecutter.app_name}}/application.ex
def start(_type, _args) do
children = [
{{ cookiecutter.app_module }}.Repo,
{% if cookiecutter.use_oban == 'y' -%}
{Oban, Application.fetch_env!(:{{ cookiecutter.app_name }}, Oban)},
{% endif -%}
# ...
]
end
4. 条件文件生成
# hooks/post_gen_project.py
#!/usr/bin/env python
from pathlib import Path
def remove_oban_files():
"""当用户选择不使用 Oban 时,删除相关迁移文件"""
oban_path = Path(
"apps",
"{{cookiecutter.app_name}}",
"priv",
"repo",
"migrations",
"20250305072314_add_oban.exs"
)
if oban_path.exists():
oban_path.unlink()
def main():
if "{{cookiecutter.use_oban}}".lower() != 'y':
remove_oban_files()
if __name__ == "__main__":
main()
测试与验证
# 清理并重新生成
rm -rf my_app
cookiecutter ./cookiecutter-phoenix --no-input
# 检查生成的项目
code my_app
| 参数 | 说明 |
|---|---|
--no-input | 使用默认值跳过交互式输入 |
输入验证
# hooks/pre_gen_project.py
#!/usr/bin/env python
app_name = "{{ cookiecutter.app_name }}"
assert app_name == app_name.lower(), f"'{app_name}' 项目名必须全小写"
总结
通过以上步骤,你可以:
- 标准化团队的项目结构
- 预配置常用依赖(如 Oban、Req 等)
- 确保代码规范一致性
- 快速初始化新项目
完整模板源码:cookiecutter-phoenix