发布于 2026-01-06 2 阅读
0

Phoenix with React:正确的方法™ DEV 的全球展示挑战赛,由 Mux 呈现:展示你的项目!

Phoenix 与 React:正确之道™

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

这是我期待已久的重写版,原文是关于如何在 React 中使用 Phoenix 的完美配置,最初发布在我的网站上。Phoenix 1.4 默认集成了Webpack,因此配置过程比以前简单得多。这篇期待已久的重写版已经完成,并且根据 Phoenix 的最新更新进行了更新。示例代码库也已更新。

如果您正在寻找我的《迁移到 TypeScript 指南》第二部分,请放心!它将在下周内完成。


最近我一直在研究Elixir。前段时间,一位朋友给我看了Discord 工程团队的一篇博客文章,讲的是他们如何利用 Elixir 的强大功能扩展平台。读完之后,我决定也尝试一下。如果你也像我一样,之前用的是 Node.js,并且正准备学习 Elixir,我建议你先看看这个入门视频

如果说 Ruby 有Rails,PHP 有Laravel,那么 Elixir 就有Phoenix。如果你用过 Rails,你会感觉非常熟悉。它具备典型 Web 框架的基本功能,同时还提供了一些很棒的附加特性,例如Channels,这使得使用 Socket 构建 Web 应用变得更加容易。

我理想的 Web 应用技术栈通常包含 React 前端。因此,我自然而然地想知道如何用 Phoenix 构建一个带有 React 前端的应用。可惜的是,将 React 与 Phoenix 集成并不像许多人想象的那么简单。我在网上找到的几乎所有教程都只讲到如何渲染单个 React 组件,而没有涵盖路由和 API 获取等关键内容。我花了不少时间,但最终还是找到了一个真正有效的方案。

所以,如果你也像我一样,一直想知道到底该怎么让它正常工作,那我就来教你。希望这能彻底解答你的疑问。

太长不看

如果您不喜欢阅读,我已经将本指南的最终结果整理在这里。完成所有设置后,您应该能够使用以下堆栈运行 Phoenix:

  • Elixir(^1.7.4
  • Node.js(^10.15.0
  • npm( ^6.4.1)
  • 凤凰(^1.4.0
  • React( ^16.7.0)
  • TypeScript( ^3.0.0)
  • Webpack( ^4.0.0)

入门

本指南假设您已经安装了ElixirPhoenixNode.js。如果您还没有安装,请在新标签页中打开上面的链接并进行安装。别担心,我会等您。

我们还将使用 Phoenix 1.4,这是撰写本文时可用的最新版本。

样板

我们将建立一个新的 Phoenix 项目,包括我们将要使用的构建环境。

从 1.4 版本开始,Phoenix 默认集成了Webpack。运行以下命令即可配置好 Phoenix,并内置对 JS 打包的支持。

$ mix phx.new phoenix_react_playground
Enter fullscreen mode Exit fullscreen mode

当系统询问你是否要同时获取和安装依赖项时,请回答“否”。我们稍后再处理。

默认情况下,该package.json文件、Webpack 配置和.babelrc文件都位于某个assets/文件夹中,而不是项目根目录。这不太理想,因为它可能会导致Visual Studio Code等 IDE 出现问题。因此,让我们将它们移动到项目根目录。

$ cd phoenix_react_playground
$ mv assets/package.json .
$ mv assets/webpack.config.js .
$ mv assets/.babelrc .
Enter fullscreen mode Exit fullscreen mode

这意味着我们需要更改 Phoenix 提供的一些默认设置:

.gitignore

@@ -26,7 +26,7 @@ phoenix_react_playground-*.tar
 npm-debug.log

 # The directory NPM downloads your dependencies sources to.
-/assets/node_modules/
+node_modules/

 # Since we are building assets from assets/,
 # we ignore priv/static. You may want to comment
Enter fullscreen mode Exit fullscreen mode

package.json

@@ -6,8 +6,8 @@
     "watch": "webpack --mode development --watch"
   },
   "dependencies": {
-    "phoenix": "file:../deps/phoenix",
-    "phoenix_html": "file:../deps/phoenix_html"
+    "phoenix": "file:deps/phoenix",
+    "phoenix_html": "file:deps/phoenix_html"
   },
   "devDependencies": {
     "@babel/core": "^7.0.0",
@@ -18,7 +18,7 @@
     "mini-css-extract-plugin": "^0.4.0",
     "optimize-css-assets-webpack-plugin": "^4.0.0",
     "uglifyjs-webpack-plugin": "^1.2.4",
-    "webpack": "4.4.0",
-    "webpack-cli": "^2.0.10"
+    "webpack": "4.28.4",
+    "webpack-cli": "^3.2.1"
   }
 }
Enter fullscreen mode Exit fullscreen mode

webpack.config.js

@@ -13,11 +13,11 @@ module.exports = (env, options) => ({
     ]
   },
   entry: {
-      './js/app.js': ['./js/app.js'].concat(glob.sync('./vendor/**/*.js'))
+    app: './assets/js/app.js'
   },
   output: {
     filename: 'app.js',
-    path: path.resolve(__dirname, '../priv/static/js')
+    path: path.resolve(__dirname, 'priv/static/js')
   },
   module: {
     rules: [
@@ -36,6 +36,10 @@ module.exports = (env, options) => ({
   },
   plugins: [
     new MiniCssExtractPlugin({ filename: '../css/app.css' }),
-    new CopyWebpackPlugin([{ from: 'static/', to: '../' }])
-  ]
+    new CopyWebpackPlugin([{ from: 'assets/static/', to: '../' }])
+  ],
+  resolve: {
+    // Add '.ts' and '.tsx' as resolvable extensions.
+    extensions: ['.ts', '.tsx', '.js', '.jsx', '.json']
+  }
 });
Enter fullscreen mode Exit fullscreen mode

上述 Webpack 配置适用于将未打包的资源放置在文件夹中的理想 Phoenix 设置assets/。我们需要确保 Phoenix 作为监听器正确运行 Webpack 命令。为此,请按config/dev.exs如下方式修改:

-  watchers: []
+  watchers: [
+    {"node", [
+      "node_modules/webpack/bin/webpack.js",
+      "--watch-stdin",
+      "--colors"
+    ]}
+  ]
Enter fullscreen mode Exit fullscreen mode

为确保一切正常运行,请运行以下命令:

$ mix deps.get
$ npm install
Enter fullscreen mode Exit fullscreen mode

一切正常吗?很好!接下来,我们将设置 TypeScript 环境。

首先,我们将为 Babel 安装 TypeScript + React 预设,并将其放入我们的.babelrc.

$ yarn add --dev @babel/preset-react @babel/preset-typescript @babel/plugin-proposal-class-properties @babel/plugin-proposal-object-rest-spread typescript
Enter fullscreen mode Exit fullscreen mode
@@ -1,5 +1,10 @@
 {
-    "presets": [
-        "@babel/preset-env"
-    ]
-}
+  "presets": [
+    "@babel/preset-env",
+    "@babel/preset-react",
+    "@babel/preset-typescript"
+  ],
+  "plugins": [
+    "@babel/plugin-proposal-class-properties",
+    "@babel/plugin-proposal-object-rest-spread"
+  ]
+}
Enter fullscreen mode Exit fullscreen mode

然后,我们将创建一个标准tsconfig.json文件,并填充以下内容。

{
  "compilerOptions": {
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "isolatedModules": true,
    "lib": ["dom", "esnext"],
    "jsx": "preserve",
    "target": "es2016",
    "module": "esnext",
    "moduleResolution": "node",
    "preserveConstEnums": true,
    "removeComments": false,
    "sourceMap": true,
    "strict": true
  },
  "include": ["./**/*.ts", "./**/*.tsx"]
}
Enter fullscreen mode Exit fullscreen mode

最后,修改 Webpack 配置,使其babel-loader能够接受 JS 和 TS 文件。别忘了同时更改 Webpack 入口文件的扩展名!

@@ -13,7 +13,7 @@ module.exports = (env, options) => ({
     ]
   },
   entry: {
-    app: './assets/js/app.js'
+    app: './assets/js/app.tsx'
   },
   output: {
     filename: 'app.js',
@@ -22,7 +22,7 @@ module.exports = (env, options) => ({
   module: {
     rules: [
       {
-        test: /\.js$/,
+        test: /\.(js|jsx|ts|tsx)$/,
         exclude: /node_modules/,
         use: {
           loader: 'babel-loader'
Enter fullscreen mode Exit fullscreen mode

设置好样板代码后,您的 Phoenix 项目的文件夹结构现在应该如下所示。

phoenix_react_playground/
├── assets/
│   ├── js/
│   │   ├── ...
│   │   └── app.tsx
│   ├── scss/
│   │   ├── ...
│   │   └── app.scss
│   └── static/
│       ├── images/
│       │   └── ...
│       ├── favicon.ico
│       └── robots.txt
├── config/
│   └── ...
├── lib/
│   └── ...
├── priv/
│   └── ...
├── test/
│   └── ...
├── .gitignore
├── mix.exs
├── package.json
├── README.md
├── tsconfig.json
└── webpack.config.js
Enter fullscreen mode Exit fullscreen mode

设置 React

现在让我们正确地将 React 与 Phoenix 连接起来。首先,当然需要安装 React。

$ yarn add react react-dom react-router-dom
$ yarn add --dev @types/react @types/react-dom @types/react-router-dom
Enter fullscreen mode Exit fullscreen mode

接下来,我们可以设置基础的 React 样板文件。在 assets 文件夹中,将其重命名app.jsapp.tsx,并按如下方式重写该文件。

assets/js/app.tsx

import '../css/app.css'

import 'phoenix_html'

import * as React from 'react'
import * as ReactDOM from 'react-dom'
import Root from './Root'

// This code starts up the React app when it runs in a browser. It sets up the routing
// configuration and injects the app into a DOM element.
ReactDOM.render(<Root />, document.getElementById('react-app'))
Enter fullscreen mode Exit fullscreen mode

assets/js/Root.tsx

import * as React from 'react'
import { BrowserRouter, Route, Switch } from 'react-router-dom'

import Header from './components/Header'
import HomePage from './pages'

export default class Root extends React.Component {
  public render(): JSX.Element {
    return (
      <>
        <Header />
        <BrowserRouter>
          <Switch>
            <Route exact path="/" component={HomePage} />
          </Switch>
        </BrowserRouter>
      </>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

assets/js/components/Header.tsx

import * as React from 'react'

const Header: React.FC = () => (
  <header>
    <section className="container">
      <nav role="navigation">
        <ul>
          <li>
            <a href="https://hexdocs.pm/phoenix/overview.html">Get Started</a>
          </li>
        </ul>
      </nav>
      <a href="http://phoenixframework.org/" className="phx-logo">
        <img src="/images/phoenix.png" alt="Phoenix Framework Logo" />
      </a>
    </section>
  </header>
)

export default Header
Enter fullscreen mode Exit fullscreen mode

assets/js/components/Main.tsx

import * as React from 'react'

const Main: React.FC = ({ children }) => (
  <main role="main" className="container">
    {children}
  </main>
)

export default Main
Enter fullscreen mode Exit fullscreen mode

assets/js/pages/index.tsx

import * as React from 'react'
import { RouteComponentProps } from 'react-router-dom'
import Main from '../components/Main'

const HomePage: React.FC<RouteComponentProps> = () => <Main>HomePage</Main>

export default HomePage
Enter fullscreen mode Exit fullscreen mode

这样应该就可以了。

现在,打开我们的项目文件夹,并按如下方式router.ex修改作用域中的路由。"/"

-    get "/", PageController, :index
+    get "/*path", PageController, :index
Enter fullscreen mode Exit fullscreen mode

然后,修改模板文件,使其能够正确加载 React 代码。在基础布局模板中,我们可以将 `<script>` 标签内的所有内容都添加到<body>脚本中。

templates/layout/app.html.eex

  <body>
    <%= render @view_module, @view_template, assigns %>
    <script type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
  </body>
Enter fullscreen mode Exit fullscreen mode

现在是首页模板。请确保将id属性设置为您在 . 中指定的应用程序入口点app.tsx

templates/page/index.html.eex

<div id="react-app"></div>
Enter fullscreen mode Exit fullscreen mode

理智检验

现在我们要检查一下一切是否正常。运行一下mix deps.get,再运行npm install一次以确保万无一失,然后运行mix ecto.setup构建数据库(如果已经设置好了)。接着运行mix phx.server,等待 Webpack 进程完成,然后前往localhost:4000

如果一切正常,网页正在加载,恭喜!接下来我们进入更精彩的部分。

屏幕截图 2019-01-20 21.24.25


使用以下方式创建其他页面react-router

现在我们已经运行了基本的 Phoenix 服务器,接下来让我们创建几个示例,展示 React 的一些强大功能。人们在演示 React 功能时最常使用的例子就是一个计数器应用。

首先,我们要在Root.tsx文件中添加 Counter 路由。

 import * as React from 'react'
 import { BrowserRouter, Route, Switch } from 'react-router-dom'

 import Header from './components/Header'
 import HomePage from './pages'
+import CounterPage from './pages/counter'

 export default class Root extends React.Component {
   public render(): JSX.Element {
     return (
       <>
         <Header />
         <BrowserRouter>
           <Switch>
             <Route exact path="/" component={HomePage} />
+            <Route path="/counter" component={CounterPage} />
           </Switch>
         </BrowserRouter>
       </>
     )
   }
 }
Enter fullscreen mode Exit fullscreen mode

然后,我们将添加该Counter组件。

assets/js/pages/counter.tsx

import * as React from 'react'
import { Link } from 'react-router-dom'

import Main from '../components/Main'

// Interface for the Counter component state
interface CounterState {
  currentCount: number
}

const initialState = { currentCount: 0 }

export default class CounterPage extends React.Component<{}, CounterState> {
  constructor(props: {}) {
    super(props)

    // Set the initial state of the component in a constructor.
    this.state = initialState
  }

  public render(): JSX.Element {
    return (
      <Main>
        <h1>Counter</h1>
        <p>The Counter is the simplest example of what you can do with a React component.</p>
        <p>
          Current count: <strong>{this.state.currentCount}</strong>
        </p>
        {/* We apply an onClick event to these buttons to their corresponding functions */}
        <button className="button" onClick={this.incrementCounter}>
          Increment counter
        </button>{' '}
        <button className="button button-outline" onClick={this.decrementCounter}>
          Decrement counter
        </button>{' '}
        <button className="button button-clear" onClick={this.resetCounter}>
          Reset counter
        </button>
        <br />
        <br />
        <p>
          <Link to="/">Back to home</Link>
        </p>
      </Main>
    )
  }

  private incrementCounter = () => {
    this.setState({
      currentCount: this.state.currentCount + 1
    })
  }

  private decrementCounter = () => {
    this.setState({
      currentCount: this.state.currentCount - 1
    })
  }

  private resetCounter = () => {
    this.setState({
      currentCount: 0
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

现在去localhost:4000/counter测试一下你的作品。如果成功,我们就可以继续下一步了。

屏幕截图 2019-01-20 21.25.24

获取 API——一个简单的示例

如前所述,我在网上找到的几乎所有 React + Phoenix 教程都只讲解了如何渲染单个 React 组件。它们似乎从未解释如何正确地配置 React 和 Phoenix,使它们能够相互通信。希望这篇文章能解答所有疑问。

开始之前,请务必确保在 `<path>` 标签内router.ex"/api"作用域声明位于路由声明的顶部/*path。真的。我花了一整周时间才弄明白我的 API 路由为什么不工作,直到最近才意识到我把路由声明的顺序弄反了。

router.ex

  # ...

  scope "/api", PhoenixReactPlaygroundWeb do
    pipe_through :api

    # ...your API endpoints
  end

  # ...

  scope "/", PhoenixReactPlaygroundWeb do
    pipe_through :browser # Use the default browser stack

    # This route declaration MUST be below everything else! Else, it will
    # override the rest of the routes, even the `/api` routes we've set above.
    get "/*path", PageController, :index
  end
Enter fullscreen mode Exit fullscreen mode

一切准备就绪后,为我们的示例数据创建一个新的上下文。

$ mix phx.gen.json Example Language languages name:string proverb:string
Enter fullscreen mode Exit fullscreen mode

router.ex

    scope "/api", PhoenixReactPlaygroundWeb do
      pipe_through :api

+     resources "/languages", LanguageController, except: [:new, :edit]
    end
Enter fullscreen mode Exit fullscreen mode

您还可以创建数据库种子数据,预先填充数据。有关如何操作的更多信息,请参阅Elixir Casts 课程

是时候再做一次健全性检查了!运行 Phoenix 服务器并访问localhost:4000/api/languages。如果一切正常,您应该会看到一个空白或已填充的 JSON(取决于您是否事先预加载了数据库)。

屏幕截图 2019-01-20 21.24.56

如果一切顺利,我们现在可以继续进行组件部分了。

Root.tsx

 import * as React from 'react'
 import { BrowserRouter, Route, Switch } from 'react-router-dom'

 import Header from './components/Header'
 import HomePage from './pages'
 import CounterPage from './pages/counter'
+import FetchDataPage from './pages/fetch-data'

 export default class Root extends React.Component {
   public render(): JSX.Element {
     return (
       <>
         <Header />
         <BrowserRouter>
           <Switch>
             <Route exact path="/" component={HomePage} />
             <Route path="/counter" component={CounterPage} />
+            <Route path="/fetch-data" component={FetchDataPage} />
           </Switch>
         </BrowserRouter>
       </>
     )
   }
 }
Enter fullscreen mode Exit fullscreen mode

pages/fetch-data.tsx

import * as React from 'react';
import { Link } from 'react-router-dom';

import Main from '../components/Main';

// The interface for our API response
interface ApiResponse {
  data: Language[];
}

// The interface for our Language model.
interface Language {
  id: number;
  name: string;
  proverb: string;
}

interface FetchDataExampleState {
  languages: Language[];
  loading: boolean;
}

export default class FetchDataPage extends React.Component<
  {},
  FetchDataExampleState
> {
  constructor(props: {}) {
    super(props);
    this.state = { languages: [], loading: true };

    // Get the data from our API.
    fetch('/api/languages')
      .then(response => response.json() as Promise<ApiResponse>)
      .then(data => {
        this.setState({ languages: data.data, loading: false });
      });
  }

  private static renderLanguagesTable(languages: Language[]) {
    return (
      <table>
        <thead>
          <tr>
            <th>Language</th>
            <th>Example proverb</th>
          </tr>
        </thead>
        <tbody>
          {languages.map(language => (
            <tr key={language.id}>
              <td>{language.name}</td>
              <td>{language.proverb}</td>
            </tr>
          ))}
        </tbody>
      </table>
    );
  }

  public render(): JSX.Element {
    const content = this.state.loading ? (
      <p>
        <em>Loading...</em>
      </p>
    ) : (
      FetchData.renderLanguagesTable(this.state.languages)
    );

    return (
      <Main>
        <h1>Fetch Data</h1>
        <p>
          This component demonstrates fetching data from the Phoenix API
          endpoint.
        </p>
        {content}
        <br />
        <br />
        <p>
          <Link to="/">Back to home</Link>
        </p>
      </Main>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

一切顺利!现在就去localhost:4000/fetch-data试试吧。

屏幕截图 2019-01-20 21.25.43


结果

如果你还在,恭喜你,你的设置已经完成!mix phx.server再运行一次,检查所有步骤。如果一切正常,那就双倍恭喜你!

现在,您可以运用这些知识来构建您的下一个 React + Phoenix 应用。本指南的最终成果已发布在此处,供大家试用。

祝你好运!如有任何疑问,欢迎随时在推特上联系我。


感谢~selsky帮忙校对这篇文章!

文章来源:https://dev.to/resir014/phoenix-with-react-the-right-way-25gi