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。