Phoenix 项目模板


Phoenix 已经有一个很好的新建项目的命令 mix phx.new。 但总有一些个性化的需求,比如我要在项目中集成 Angular 前端,或添加好一些会使用的依赖。

这里我要创建的是一个 Restful API 项目模板的示例,用到的工具是 Python Cookiecutter

基础配置

依赖项列表

  • Cookiecutter 2.6+
  • Elixir 1.18+
  • Erlang 27.2+
  • Phoenix 1.17+
  • Python 3.10+
  • VSCode 1.97+

工具安装

# for mac
brew install cookiecutter
# for linux
pip install --user cookiecutter

模板初始化

# 创建项目目录
mkdir cookiecutter-phoenix && cd cookiecutter-phoenix
# 创建 cookiecutter 配置文件
cat <<EOF > 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",
}
EOF

# 创建项目
mix phx.new awesome_app --no-html \
    --no-assets \
    --no-gettext \
    --no-dashboard \
    --no-live \
    --no-mailer

# 创建依赖版本文件, 方便使用 asdf 安装相关版本
cat <<EOF > awesome_app/.tool-versions
erlang {{cookiecutter.erlang_version}}
elixir {{cookiecutter.elixir_version}}
EOF

模板定制

内容替换策略

  • 文件及文件夹重命名
  • 正则表达式替换文件内容(可以使用 vscode 搜索替换功能)
  • 模板转义处理
# for mac
# 替换文件内容
find ./awesome_app -depth -name '*awesome_app*' -execdir sh -c 'mv "$1" "$(echo "$1" | sed "s/awesome_app/{{cookiecutter.app_name}}/g")"' _ {} \;

# 替换文件及文件夹名称
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' {} +
# 再使用 vscode 搜索替换功能检查是否有遗漏

# 转义处理
# 类似 mod: {{{{ cookiecutter.app_module }}.Application, []} 可能需要替换为下面的形式
mod: {{'{'}}{{ cookiecutter.app_module }}.Application, []{{'}'}}

# 如果是大段代码,使用下面的形式
{% raw -%}
{:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
{% endraw -%}

本地扩展开发

注册全局模板函数或 filters

# File: 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"Error fetching {name} version: {e}")
        return deps_default_versions[name]


class CustomExtension(Extension):
    def __init__(self, environment):
        super(CustomExtension, self).__init__(environment)
        environment.globals['gen_secret'] = lambda v: secrets.token_urlsafe(v)
        environment.globals['latest_version'] = latest_package_version

加载扩展

// File: cookiecutter.json
{
  "app_name": "my_app",
  ...,
  "_extensions": ["local_extensions.CustomExtension"]
}

使用模板函数

# File: config/dev.exs
config :{{ cookiecutter.app_name }}, {{ cookiecutter.app_module }}Web.Endpoint,
  # Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
  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 手动添加的文档,如有问题请参考官方文档。

添加选项

// File cookiecutter.json
{
  ...,
  "use_oban": "y",
}

依赖管理

# File mix.exs
defp deps do
  [
    ...,
    {% if cookiecutter.use_oban == 'y' -%}
    {:oban, "{{ latest_version('oban') }}"},
    {% endif -%}
  ]
end

相关配置

# File config/config.exs
{% if cookiecutter.use_oban == 'y' -%}
# config Oban
config :{{ cookiecutter.app_name }}, Oban,
  engine: Oban.Engines.Basic,
  queues: [default: 10],
  repo: {{ cookiecutter.app_module }}.Repo,
  prefix: "oban"
{% endif -%}

# File config/test.exs
{% if cookiecutter.use_oban == 'y' -%}
# config Oban
config :{{ cookiecutter.app_name }}, Oban, testing: :manual
{% endif -%}

# File 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

依赖文件生成

添加 oban migration 文件

# File priv/repo/migrations/20250305072314_add_oban.exs
defmodule MyApp.Repo.Migrations.AddObanJobsTable do
  use Ecto.Migration

  def up do
    Oban.Migration.up()
  end

  def down do
    Oban.Migration.down(version: 1)
  end
end

根据条件生成文件

# File hooks/post_gen_project.py
#!/usr/bin/env python

from pathlib import Path
import subprocess

def remove_oban_files():
    oban_migration_path = Path(
        "apps",
        "{{cookiecutter.app_name}}",
        "priv",
        "repo",
        "migrations",
        )
    target_file = oban_migration_path / "20250305072314_add_oban.exs"
    if target_file.exists():
        target_file.unlink()


def main():
    # 条件删除 Oban 迁移文件
    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 直接使用默认值

输入验证

# File hooks/pre_gen_project.py
#!/usr/bin/env python

app_name = ":{{ cookiecutter.app_name }}"
assert app_name == app_name.lower(), "'{}' app name should be all lowercase".format(app_name)

通过上面的步骤,我们可以快速方便的创建自己的项目模板,并且可以根据自己的需求进行扩展。

相关代码仓库地址:cookiecutter-phoenix