Phoenix 资产管线完全指南:从 esbuild、Tailwind 到自定义构建脚本
【免费下载链接】phoenixPeace of mind from prototype to production项目地址: https://gitcode.com/gh_mirrors/ph/phoenix
本文基于 Phoenix 官方指南 guides/asset_management.md 整理而成,全面讲解 Phoenix v1.7+ 默认的资产(Asset)管理方案:如何用 esbuild 打包 JavaScript、用 Tailwind 编译 CSS、引入第三方 JS 包、处理图片字体等外部资源,以及如何替换默认构建工具(esbuild、Tailwind、图标库)。读完本文,你将掌握从mix setup到mix assets.deploy的完整资产构建链路,并能根据项目需求自由定制构建脚本。
一、Phoenix 资产管线概览:告别 Node.js 依赖
除了生成 HTML,绝大多数 Web 应用还需要处理各类静态资源:JavaScript、CSS、图片、字体等。从 Phoenix v1.7 起,新生成的应用通过 esbuild(经由 Elixir 的 esbuild 封装库)和 tailwindcss(经由 Elixir 的 tailwindcss 封装库)来准备资产。这种直接集成意味着:新应用不再依赖 Node.js 或外部构建系统(如 Webpack),纯 Elixir 环境即可完成全部构建。
Phoenix 资产的默认流向非常清晰:
- JavaScript 源码放在
assets/js/app.js,由esbuild打包输出到priv/static/assets/js/app.js; - 开发环境下,这一过程由
esbuild的 watcher(文件监听器)自动完成; - 生产环境下,通过运行
mix assets.deploy完成构建; esbuild也能处理 CSS,但默认情况下 CSS 全部交由tailwind构建;- 其余无需预处理的静态资源(图片、字体、favicon 等)直接放入
priv/static目录。
在仓库的安装器模板 installer/templates/phx_single/config/config.exs.eex 中可以看到新应用的默认配置:esbuild版本锁定为0.25.4,tailwind版本为4.3.0。Phoenix 项目自身也采用同样的策略——在 config/config.exs 中,Phoenix 用 esbuild 0.25.4 将自己的assets/js/phoenix源码分别打包为 ESM(phoenix.mjs)、CJS(phoenix.cjs.js)和浏览器全局版(phoenix.js/phoenix.min.js),并在 mix.exs 中定义了assets.build、assets.watch别名,这正是本文要讲的构建机制在 Phoenix 自身项目中的实践。
二、引入第三方 JS 包:三种可选方案
如果你的应用需要引入 JavaScript 依赖,有以下三种途径:
方案一:本地内置(Vendor)
把依赖源码直接放进项目里,然后在assets/js/app.js中用相对路径导入:
import topbar from "../vendor/topbar"这种方式最简单直接,不引入任何包管理工具,缺点是升级依赖需要手动同步源码。
方案二:使用 npm 管理
在assets目录下执行npm install topbar --prefix assets,这会在assets目录内创建package.json和package-lock.json,esbuild会自动识别并解析这些依赖:
import topbar from "topbar"为了确保在检出项目或构建 release 时自动安装依赖,需要在mix.exs的assets.deploy和assets.build步骤中加入"cmd --cd assets npm ci":
"assets.build": ["cmd --cd assets npm ci", "tailwind your_app", "esbuild your_app"], "assets.deploy": [ "cmd --cd assets npm ci", "tailwind your_app --minify", "esbuild your_app --minify", "phx.digest" ]方案三:通过 Mix 从源码仓库跟踪依赖
在mix.exs中声明一个 git 依赖:
# mix.exs {:topbar, github: "buunguyen/topbar", app: false, compile: false}运行mix deps.get拉取依赖,然后照常导入:
import topbar from "topbar"新生成的应用正是用这种方案引入图标(如 Heroicons),好处有三:不必在项目里内置一份所有图标的副本、不需要额外安装npm等系统依赖、同时还能通过 Mix 锁定精确版本。需要注意的是:git 依赖无法被 Hex 包使用,如果计划把项目发布到 Hex,需要改用其他方案。
提示:如果使用了第三方 JS 包管理器,可能需要调整部署步骤以正确包含这些包。若使用
mix phx.gen.release --docker生成 Docker 部署,请参考 Mix.Tasks.Phx.Gen.Release 中关于 Docker 的文档说明。
三、图片、字体与外部文件:--external与 loader
当 CSS 或 JS 中引用了外部文件时,esbuild默认会尝试校验并管理它们。例如,在 CSS 中引用priv/static/images/bg.png(通过/images/bg.png对外提供):
body { background-image: url(/images/bg.png); }此时构建可能报错:
error: Could not resolve "/images/bg.png" (mark it as external to exclude it from the bundle)由于这些图片已由 Phoenix 静态文件服务管理,你需要按报错提示把/images(以及/fonts)下的资源标记为 external。自 Phoenix v1.6.1+ 起,新应用默认就带上了这一配置,位于config/config.exs:
args: ~w( js/app.js --bundle --format=esm --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/* --alias:@=. ),如果还需要引用其他目录,请相应更新上述参数。另外,运行mix phx.digest会为priv/static中所有资产生成带内容指纹(digest)的文件,所以你的图片和字体依然能获得缓存失效(cache-busting)能力。关于 digest 的底层实现可参见 Phoenix.Digester 及 Phoenix.Digester.Gzip(后者负责对.js/.map/.css/.txt/.text/.html/.json/.svg/.eot/.ttf等扩展名做 gzip 预压缩,见 mix.exs 中的gzippable_exts配置)。
第三方库的字体与图片无法加载怎么办?
如果你导入的 Node 包依赖额外的字体或图片,你可能会发现它们加载失败。原因在于:这些资源虽然在 JS/CSS 中被引用,但默认情况下 esbuild 不会处理或复制被引用的文件。解决办法是在config/config.exs中为 esbuild 增加 loader 参数,让被引用的资源被复制到输出目录。下面的例子会把所有被引用的字体文件复制到输出目录:
args: ~w( js/app.js --bundle --format=esm --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/* --alias:@=. --loader:.woff=copy --loader:.ttf=copy --loader:.eot=copy --loader:.woff2=copy ),更多细节可参考 esbuild 官方文档中关于 copy loader 的内容类型。
四、使用 esbuild 插件:自定义构建脚本
Phoenix 默认的 esbuild 配置(经由 Elixir 封装库)不支持 esbuild 插件。如果你想使用插件——比如用 esbuild 把 SASS 编译成 CSS——就需要用自定义构建脚本替换默认构建系统。
准备环境
首先需要在开发环境安装 Node.js,并确保生产构建步骤也能访问它。然后在assets目录下把esbuild加入 Node.js 包,并安装 Phoenix 相关的 JS 包:
$ npm install esbuild --save-dev $ npm install ../deps/phoenix ../deps/phoenix_html ../deps/phoenix_live_view --save或使用 Yarn:
$ yarn add --dev esbuild $ yarn add ../deps/phoenix ../deps/phoenix_html ../deps/phoenix_live_view编写自定义构建脚本
新建assets/build.js:
const esbuild = require("esbuild"); const args = process.argv.slice(2); const watch = args.includes('--watch'); const deploy = args.includes('--deploy'); const loader = { // Add loaders for images/fonts/etc, e.g. { '.svg': 'file' } }; const plugins = [ // Add and configure plugins here ]; // Define esbuild options let opts = { entryPoints: ["js/app.js"], bundle: true, logLevel: "info", target: "es2022", outdir: "../priv/static/assets", external: ["*.css", "fonts/*", "images/*"], nodePaths: ["../deps"], loader: loader, plugins: plugins, }; if (deploy) { opts = { ...opts, minify: true, }; } if (watch) { opts = { ...opts, sourcemap: "inline", }; esbuild .context(opts) .then((ctx) => { ctx.watch(); }) .catch((_error) => { process.exit(1); }); } else { esbuild.build(opts); }这个脚本覆盖以下使用场景:
node build.js:为开发与测试构建(CI 上很有用);node build.js --watch:同上,但持续监听文件变化;node build.js --deploy:为生产环境构建压缩版资产。
接入 Phoenix 的三步配置
第一步,修改config/dev.exs,让脚本在文件变化时自动运行,替换原:esbuild在watchers下的配置:
config :hello, HelloWeb.Endpoint, ... watchers: [ node: ["build.js", "--watch", cd: Path.expand("../assets", __DIR__)] ], ...第二步,修改mix.exs中的aliases,让mix setup安装 npm 包,并让mix assets.deploy使用新的 esbuild:
defp aliases do [ setup: ["deps.get", "ecto.setup", "cmd --cd assets npm install"], ..., "assets.deploy": ["cmd --cd assets node build.js --deploy", "phx.digest"] ] end第三步,删除config/config.exs中的 esbuild 配置,并从mix.exs的deps函数中移除 esbuild 依赖,至此完成切换。
作为对照,仓库安装器模板 installer/templates/phx_single/mix.exs.eex 中展示了新应用的默认别名结构:assets.setup依次执行各构建器的install --if-missing,assets.build执行compile加各构建器,assets.deploy则对每个构建器加--minify后执行phx.digest;开发环境的 watcher 配置见 installer/templates/phx_single/config/dev.exs.eex(esbuild: {Esbuild, :install_and_run, [:your_app, ~w(--sourcemap=inline --watch)]}与tailwind: {Tailwind, :install_and_run, [:your_app, ~w(--watch)]})。
五、替换 JS 构建工具:移除 esbuild
如果你开发的是纯 API,或想换用其他资产构建工具,可以移除esbuildHex 包,然后遵循所选第三方工具自身的步骤。移除 esbuild 共四步:
- 删除
config/config.exs和config/dev.exs中的 esbuild 配置; - 删除
mix.exs中定义的assets.deploy任务; - 从
mix.exs移除 esbuild 依赖; - 解锁 esbuild 依赖:
$ mix deps.unlock esbuild六、替换 CSS 框架:移除 tailwind
默认情况下,Phoenix 使用tailwind库及其默认插件生成 CSS(新应用还默认启用了 daisyUI 等插件,见 installer/templates/phx_assets/app.css.eex)。如果你想使用外部的 tailwind 插件或其他 CSS 框架,应替换tailwindHex 包(步骤见下),之后既可以用 esbuild 插件(如第四节所述),也可以直接引入一套独立的框架。
移除 tailwind 的步骤与移除 esbuild 类似:
- 删除
config/config.exs和config/dev.exs中的 tailwind 配置; - 删除
mix.exs中定义的assets.deploy任务; - 从
mix.exs移除 tailwind 依赖; - 解锁 tailwind 依赖:
$ mix deps.unlock tailwind如果不再需要,也可以一并移除并删除heroicons依赖。
七、替换图标库:以 Remix Icon 为例
Phoenix 内置了 Heroicons 展示了这一机制:它是一个 Tailwind 插件,遍历deps/heroicons/optimized下的四个尺寸目录(24/outline、24/solid、20/solid、16/solid),将每个 SVG 文件编码为data:image/svg+xmlURL 并注册为hero-*组件类,同时在assets/css/app.css中通过@plugin "../vendor/heroicons";挂载。
如果你偏爱其他图标集,可以改造这段内嵌代码。下面以 Remix Icon 为例:
第一步,把mix.exs中的heroicon仓库替换为remixicons:
{:remixicons, github: "Remix-Design/RemixIcon", sparse: "icons", tag: "v4.6.0", app: false, compile: false, depth: 1},第二步,把遍历 heroicons 依赖的assets/vendor/heroicons.js替换为遍历 remix icons 的assets/vendor/remixicons.js:
const plugin = require("tailwindcss/plugin") const fs = require("fs") const path = require("path") module.exports = plugin(function({matchComponents, theme}) { let baseDir = path.join(__dirname, "../../deps/remixicons/icons"); let values = {}; let icons = fs .readdirSync(baseDir, { withFileTypes: true }) .filter((dirent) => dirent.isDirectory()) .map((dirent) => dirent.name); icons.forEach((dir) => { fs.readdirSync(path.join(baseDir, dir)).map((file) => { let name = path.basename(file, ".svg"); values[name] = { name, fullPath: path.join(baseDir, dir, file) }; }); }); matchComponents( { ri: ({ name, fullPath }) => { let content = fs .readFileSync(fullPath) .toString() .replace(/\r?\n|\r/g, ""); return { [`--ri-${name}`]: `url('data:image/svg+xml;utf8,${content}')`, "-webkit-mask": `var(--ri-${name})`, mask: `var(--ri-${name})`, "background-color": "currentColor", "vertical-align": "middle", display: "inline-block", width: theme("spacing.10"), height: theme("spacing.10"), }; }, }, { values }, ); })第三步,修改assets/css/app.css,改为导入你的新插件。
第四步,更新lib/my_app_web/components/core_components.ex中的icon函数,改为匹配ri-前缀。在新应用模板 installer/templates/phx_web/components/core_components.ex.eex 中,icon函数只处理hero-前缀的图标(如hero-information-circle、hero-x-mark、hero-arrow-path等),替换后则按如下方式匹配 Remix 图标:
@doc """ Renders a Remix Icon. You can customize the size and colors of the icons by setting width, height, and background color classes. ## Examples <.icon name="ri-github-fill" /> <.icon name="ri-github" class="ml-1 w-3 h-3 animate-spin" /> """ attr :name, :string, required: true attr :class, :any, default: "size-5" def icon(%{name: "ri-" <> _} = assigns) do ~H""" <i class={[@name, @class]} aria-hidden="true"></i> """ end完成以上步骤后,把应用里的 Heroicons 换成 Remix 图标即可。这套思路对其他图标库同样适用:核心工作就是写一个 Tailwind 插件去遍历对应图标库的 SVG,生成合适的 CSS 类。另外,部分图标集也以常规 Hex 包的形式提供,可以进一步简化集成。
结语
Phoenix 的资产管线设计围绕"零 Node.js 依赖"与"默认配置开箱即用"展开:开发时 watcher 实时构建,发布时mix assets.deploy一键产出压缩资产并配合phx.digest生成指纹缓存。当默认能力无法满足需求时,无论是要用 esbuild 插件、更换 JS/CSS 构建工具,还是替换图标库,Phoenix 都提供了清晰的替换路径。理解这条管线,能让你在从原型到生产的路上,对前端资源的构建、部署与缓存机制做到心中有数。
【免费下载链接】phoenixPeace of mind from prototype to production项目地址: https://gitcode.com/gh_mirrors/ph/phoenix
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考