使用 Cookiecutter 创建 Phoenix 项目模板


虽然 Phoenix 提供了 mix phx.new 命令快速创建项目,但团队协作或个人习惯往往需要统一的代码规范、预配置的依赖项和标准化的项目结构。

本文将介绍如何基于 Cookiecutter 打造一个可复用的 Phoenix 项目模板。

环境要求

工具版本要求
Cookiecutter2.6+
Elixir1.18+
Erlang27.2+
Phoenix1.17+
Python3.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