緣起
我是一個Ruby程序員,最近開始學習Elixir。我驚嘆于Elixir和Phoenix展現的技術魅力,并很快喜歡上了這個新東西。就像Rails之于Ruby一樣,Phoenix使得Elixir變得流行起來,原因就在于Elixir使得開發人員能夠高效的編寫出性能優秀,穩定性好的應用程序,并且很容易使用這些應用處理實時數據。
寫這篇博文時,我只有大約一周的Phoenix使用經驗。我寫這篇博文的目的就是趨勢自己從不同的角度思考正在解決的問題,以期獲得對這門語言和框架更深入的理解。如果你在代碼中發現任何錯誤或有改進的建議,歡迎給我來信或者提交pull request!
如果你和我一樣也是個Elixir新手,我推薦你閱讀 Programming Elixir 和 Programming Phoenix ,這兩本書全面的闡述了Elixir和Phoenix的基本要點。
我們要做什么
為了向我喜愛的聊天應用Slack致敬,我將打造一個高仿版的Slack,我叫它Sling。為了讓這篇博文簡潔、完整、可讀,我只實現Slack的部分功能,但是要實現的這部分功能足夠我們習得Phoenix的基本原理。
Slack有team的概念,并且每個team有若干個channel。team成員能夠加入到channel,channel就是聊天的地方。簡化起見,我們就不創建team這個功能了,取而代之的是我們將創建room,每個注冊用戶都能加入到room中,并且在room中聊天。
我們會使用牛x的Phoenix Presence Module展示當前room中的在線用戶。
我將盡可能詳盡的展示出我是如何構建這個應用的,為此我會保持小增量的git commit。并且在每次提交后留下git diff的鏈接。
技術棧
服務器端
前端
由于我的技術經驗是使用Ruby構建web應用,所以熟悉Rails的讀者對于我寫的東西會更易于理解。我假定你熟悉JavaScript和ES6。由于這不是React 教程,所以我會盡量解釋React組件相關的邏輯,但是不會深究。
如果你還沒有安裝Elixir或者Phoenix,請看這里
項目結構
對于我們要構建的應用而言,真實項目一般會創建兩個獨立的代碼倉庫,一個用于放置Phoenix API,另一個用于放置React App。但是為了使我們的博文清楚明了,我將代碼放置在同一個倉庫中。目錄結構如下:
sling/
|--- api/
(phoenix app)
|--- web/
(react app)
開始吧!少年。
創建Phoenix應用
創建一個新的文件夾作為我們的代碼倉庫
mkdir sling
cd sling
生成新的Phoenix應用,我們使用Phoenix直接作為JSON API。所以不需要默認安裝的asset manager, 使用參數--no-brunch
;也不需要html模板和瀏覽器端的路由,使用參數--no-html
mix phoenix.new sling --no-html --no-brunch
mv sling api
創建React應用
使用 create-react-app初始化React App,這是一個強大的工具,零配置搭建我們的前端應用。
安裝 create-react-app
命令行工具
npm i -g create-react-app
創建React App
create-react-app sling
mv sling web
牛叉吧,我們已經初始化好了后端的Phoenix API和前端的React App。
我們第一個正式的提交 init commit
配置Phoenix項目
首先配置數據庫,開發環境下默認的數據庫配置位于該文件sling/api/config/dev.exs
, PostgreSQL默認用戶密碼均為 postgres 。安全起見我們新建一個文件dev.secret.exs
,用于存放私人的數據庫配置信息,覆蓋掉默認的數據庫連接配置。這樣一來也便于別人使用我們的代碼。將dev.secret.exs
加入到.gitignore中(由于新建的配置文件是私有信息所以不必提交),內容如下:
sling/api/config/dev.secret.exs
use Mix.Config
config :sling, Sling.Repo,
username: "your_postgres_user",
password: "your_postgres_password"
在dev.exs
的末尾添加 import "dev.secret.exs"
,這樣我們的私有配置才能生效。
sling/api/config/dev.exs
# contents above
import_config "dev.secret.exs"
創建數據庫(當前所在路徑為sling/api
)
mix ecto.create
數據庫創建完成后,啟動Phoenix App 。
mix phoenix.server
訪問 http://localhost:4000, 正常情況會報錯,原因在于我們的Phoenix App 只用做API,沒有配置網頁瀏覽相關的路由。
Phoenix已經配置完成,接下來我們配置 React App。
配置React項目
create-react-app已近初始化了一個可運行的app, npm start
, 訪問 http://localhost:3000,就能看到初始化的react app。我們要配置自己的redux react-router, 所以刪除掉web/src
目錄下的所有文件。
另,我們將使用最新的JavaScript依賴管理工具Yarn, 安裝指南在此.
在這個前端應用中有許多第三方庫我們需要使用,一次性全部將其安裝(當前目錄是sling/web
)
yarn add aphrodite lodash md5 moment phoenix react-redux react-router@4.0.0-alpha.5 redux redux-form redux-thunk
你應該注意到我們使用v4-alpha版本的react-router, 其相較于v2版本的react-router有很多重大的改變。借這個機會我們一并學一學v4-alpha版react-router, 期待v4正式版盡快發布,如有變化到時我會更新博文。
我將使用Airbnb's styleguide,其中用到了eslint和flow,所以接下來我們安裝開發環境下用到的第三方庫。
yarn add babel-eslint eslint eslint-config-airbnb eslint-plugin-flowtype eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react --dev
Linting rules的配置完全依賴于個人的喜好,下面是我自己用的.eslintrc
文件。(react/no-unused-prop-types 規則被disable掉了,原因是和flowtype有沖突)
sling/web/.eslintrc
{
"parser": "babel-eslint",
"plugins": ["react", "flowtype"],
"extends": ["airbnb", "plugin:flowtype/recommended"],
"rules": {
"react/jsx-filename-extension": 0,
"import/prefer-default-export": 0,
"react/no-unused-prop-types": 0,
"camelcase": 0
},
"globals": {
"fetch": true,
"window": true,
"document": true,
"localStorage": true
}
}
配置React/Redux
React項目有各種各樣的目錄結構,當然都是基于應用場景權衡的結果。就我們的項目而言,創建containers目錄用于存放和redux store 連接相關的組件。創建components目錄,存放其他組件。創建actions和reducers目錄分別用于存放action和reducer相關的文件。創建store目錄存放redux store相關的配置文件。我們著手開始吧。
創建app的入口文件 sling/web/src/index.js, 這個文件需要導入redux store配置文件(稍后創建), App容器組件,并掛載到 index.html 的<div id="root" />
節點下。
sling/web/src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import App from './containers/App';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>
, document.getElementById('root')
);
創建上面提到的redux store配置文件,引入reducers文件(稍后創建),使用redux-thunk 處理異步操作和Promises。
sling/web/src/store/index.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducers from '../reducers';
const middleWare = [thunk];
const createStoreWithMiddleware = applyMiddleware(...middleWare)(createStore);
const store = createStoreWithMiddleware(reducers);
export default store;
創建reducer根文件sling/web/src/reducers/index.js,這個文件用于匯總其他的reducer文件,但是現在我們只需要用redux-form使reducer 能正常工作即可。我們不直接返回配置參數的combineReducers函數,相反,當logout時我們強制返回帶undefined參數的appReducer,這樣就會強制初始化所有reducer的state(也就是強制清理登出用戶的redux state,不會讓其污染下一個login用戶的state)
sling/web/src/reducers/index.js
import { combineReducers } from 'redux';
import { reducer as form } from 'redux-form';
const appReducer = combineReducers({
form,
});
export default function (state, action) {
if (action.type === 'LOGOUT') {
return appReducer(undefined, action);
}
return appReducer(state, action);
}
現在我們來創建App組件,在這個組件中我們要到了v4版的react-router 來配置頁面路由。目前我們只有兩個路由,一個是Home路由,另一個是404頁面。
sling/web/src/containers/App/index.js
// @flow
import React, { Component } from 'react';
import { BrowserRouter, Match, Miss } from 'react-router';
import Home from '../Home';
import NotFound from '../../components/NotFound';
class App extends Component {
render() {
return (
<BrowserRouter>
<div>
<Match exactly pattern="/" component={Home} />
<Miss component={NotFound} />
</div>
</BrowserRouter>
);
}
}
export default App;
目前我們的Home頁面只是簡單的組件
sling/web/src/containers/Home/index.js
// @flow
import React from 'react';
const Home = () => (<div>Home</div>);
export default Home;
NotFound組件
sling/web/src/components/NotFound/index.js
// @flow
import React from 'react';
import { Link } from 'react-router';
const NotFound = () =>
<div style={{ margin: '2rem auto', textAlign: 'center' }}>
<p>Page not found</p>
<p><Link to="/">Go to the home page →</Link></p>
</div>;
export default NotFound;
好,redux的基本配置已經完成。
到目前為止,我們的前端App和后端API還無法通訊,不過也好,本篇博文就此結束。下篇博文我們將實現前端和后端的通訊,并且添加用戶賬戶和用戶身份認證。