這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。在之前的文章中,我們看了一些使用 go-micro 和 go 語言的產生的各種事件驅動的方法。 在本篇文章,我們將深入到用戶端,探究一下如何建立一個能夠與我們之前建立的平台互動的 網頁用戶端。這篇文章會介紹如何使用 [micro](https://github.com/micro/micro) 工具包產生 web 用戶端從外部代理內部 rpc 方法。我們會建立一個 user 介面用於產生平台的登入介面、還會建立一個介面用於使用我們的 consignments。該介面包含了建立使用者、登入、和建立 consignments 等功能。 本系列的前幾篇文章已經介紹過其中的部分代碼了,在這篇文章我會帶大家深入瞭解一下。所以讓我們開始吧!## RPC 複興REST 已經在網路上服務了很多年了,並且迅速成為管理用戶端和伺服器之間資源的途徑。REST 正在逐漸取代已經過時的 RPC 和 SOAP。曾經必須寫一個 wsdl 檔案的時代已經過去了。REST 向我們承諾了一種實用,簡單和標準化的資源管理方法。 REST 使用 http 協議明確了正在執行的具體 web 動作類型。REST 鼓勵我們使用 http 錯誤響應碼來更精確地描述伺服器的響應狀態。而且大多數情況下,這種方法運行良好,並沒有問題。但是像所有好東西一樣,REST有許多不足和缺點,我不打算在這裡詳細介紹。大家有興趣可以參考[這篇文章](https://medium.freecodecamp.org/rest-is-the-new-soap-97ff6c09896d)。但是!隨著**微服務**的出現,RPC 正在捲土重來。REST 對於管理不同的資源非常有用,但微服務通常只處理單一資源,這一性質導致我們不需要在微服務的上下文中使用 RESTful 術語。相反,我們可以專註於每個服務的單一的具體操作和互動。## Micro我們已經在本系列教程中廣泛使用了 go-micro,現在我們將介紹 micro cli 這個工具包。這個 micro 工具包提供了的功能包括 API Gateway、 sidecar、Web Proxy以及其他一些很酷的功能。但是這篇文章我們使用到的功能主要是 API Gateway。API Gateway將允許我們將 rpc 調用代理為 Web 友好的 javascriptON rpc 調用,然後將用戶端應用程式中使用的 url 暴露出來。那麼以上這些炫酷功能是如何工作的? 首先要確保安裝了 micro 工具包:```$ go get -u github.com/micro/micro```Docker環境下使用 Micro 更好的方法還是建議大家使用Docker鏡像:```$ docker pull microhq/micro```接下來可以看一下 user 服務的代碼,我對 user 服務的代碼做了一些錯誤處理和命名規範方面的修改:```go// shippy-user-service/main.gopackage mainimport ("log"pb "github.com/EwanValentine/shippy-user-service/proto/auth""github.com/micro/go-micro"_ "github.com/micro/go-plugins/registry/mdns")func main() {// 建立了一個資料庫 connection // main 方法結束之前要關閉資料庫連接db, err := CreateConnection()defer db.Close()if err != nil {log.Fatalf("Could not connect to DB: %v", err)}// 將 user 結構類型自動移植到資料庫類型中。此操作在服務每一次重啟時都會做一次檢測db.AutoMigrate(&pb.User{})repo := &UserRepository{db}tokenService := &TokenService{repo}// 建立一個新的服務srv := micro.NewService(// 這個名字必須於你在protobuf definition定義的包名匹配micro.Name("shippy.auth"),)// Init 用於初始化命令列參數srv.Init() // Will comment this out for now to save having to run this locally... // publisher := micro.NewPublisher("user.created", srv.Client())// 註冊 handlerpb.RegisterAuthHandler(srv.Server(), &service{repo, tokenService, publisher})// 啟動 serverif err := srv.Run(); err != nil {log.Fatal(err)}}``````c// shippy-user-service/proto/auth/auth.protosyntax = "proto3";package auth;service Auth {rpc Create(User) returns (Response) {}rpc Get(User) returns (Response) {}rpc GetAll(Request) returns (Response) {}rpc Auth(User) returns (Token) {}rpc ValidateToken(Token) returns (Token) {}}message User {string id = 1;string name = 2;string company = 3;string email = 4;string password = 5;}message Request {}message Response {User user = 1;repeated User users = 2;repeated Error errors = 3;}message Token {string token = 1;bool valid = 2;repeated Error errors = 3;}message Error {int32 code = 1;string description = 2;}``````go// shippy-user-service/handler.gopackage mainimport ("errors""fmt""log"pb "github.com/EwanValentine/shippy-user-service/proto/auth"micro "github.com/micro/go-micro""golang.org/x/crypto/bcrypt""golang.org/x/net/context")const topic = "user.created"type service struct {repo RepositorytokenService AuthablePublisher micro.Publisher}func (srv *service) Get(ctx context.Context, req *pb.User, res *pb.Response) error {user, err := srv.repo.Get(req.Id)if err != nil {return err}res.User = userreturn nil}func (srv *service) GetAll(ctx context.Context, req *pb.Request, res *pb.Response) error {users, err := srv.repo.GetAll()if err != nil {return err}res.Users = usersreturn nil}func (srv *service) Auth(ctx context.Context, req *pb.User, res *pb.Token) error {log.Println("Logging in with:", req.Email, req.Password)user, err := srv.repo.GetByEmail(req.Email)log.Println(user, err)if err != nil {return err}// 比較輸入的密碼與儲存在資料庫裡的雜湊密碼if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {return err}token, err := srv.tokenService.Encode(user)if err != nil {return err}res.Token = tokenreturn nil}func (srv *service) Create(ctx context.Context, req *pb.User, res *pb.Response) error {log.Println("Creating user: ", req)// 為我們的密碼產生一個雜湊值hashedPass, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)if err != nil {return errors.New(fmt.Sprintf("error hashing password: %v", err))}req.Password = string(hashedPass)if err := srv.repo.Create(req); err != nil {return errors.New(fmt.Sprintf("error creating user: %v", err))}res.User = reqif err := srv.Publisher.Publish(ctx, req); err != nil {return errors.New(fmt.Sprintf("error publishing event: %v", err))}return nil}func (srv *service) ValidateToken(ctx context.Context, req *pb.Token, res *pb.Token) error {// Decode tokenclaims, err := srv.tokenService.Decode(req.Token)if err != nil {return err}if claims.User.Id == "" {return errors.New("invalid user")}res.Valid = truereturn nil}```現在運行 `$ make build && make run`。 然後轉到 shippy-email-service 運行`$ make build && make run`。 一旦這兩個服務都運行,運行:```shell$ docker run -p 8080:8080 \ -e MICRO_REGISTRY=mdns \microhq/micro api \--handler=rpc \--address=:8080 \--namespace=shippy ```這將在 Docker 容器中開一個 8080 連接埠上,該連接埠將 micro api-gateway 作為 rpc 處理常式暴露出來,使用 mdns 作為本地的註冊表,使用命名空間 shippy,shippy 是我們所有服務名稱的第一部分。例如 shippy.auth 或 shippy.email。設定它是很重要的,因為它預設為 go.micro.api,在預設情況下,go.micro.api 是無法找到我們需要的特定服務來進行代理的。我們現在可以使用以下方式調用我們的 user 服務方法:建立一個 user:```shellcurl -XPOST -H 'Content-Type: application/javascripton' \-d '{ "service": "shippy.auth", "method": "Auth.Create", "request": { "user": { "email": "ewan.valentine89@gmail.com", "password": "testing123", "name": "Ewan Valentine", "company": "BBC" } } }' \ http://localhost:8080/rpc```這個請求中包含了我們想要傳送給的服務名、要使用的服務方法、以及服務資料。驗證使用者:```shell$ curl -XPOST -H 'Content-Type: application/javascripton' \ -d '{ "service": "shippy.auth", "method": "Auth.Auth", "request": { "email": "your@email.com", "password": "SomePass" } }' \http://localhost:8080/rpc```## Consignment service現在再次啟動我們的 consignment 服務,`$ make build && make run`。 我們不需要在這裡改變任何東西,但是,運行 rpc 代理的話我們還應該建立一個 consignment:```shell$ curl -XPOST -H 'Content-Type: application/javascripton' \ -d '{"service": "shippy.consignment","method": "ConsignmentService.Create","request": {"description": "This is a test","weight": "500","containers": []}}' --url http://localhost:8080/rpc```## Vessel service最後為了測試使用者介面介面,我們需要運行 vessel 服務,這裡沒有對代碼有什麼修改,直接運行 `$ make build && make run` 即可。## User interface現在可以使用我們的剛剛建立的新 rpc 節點建立一個使用者介面。本文使用了 React,當然如果你喜歡的話可以使用其餘的架構。請求都是一樣的。本文使用來自 Facebook 的 react-create-app 庫: `$ npm install -g react-create-app` 安裝完成後,執行 `$ react-create-app shippy-ui`。 這將為您建立一個 React 應用程式的架構。```javascript// shippy-ui/src/App.javascriptimport React, { Component } from 'react';import './App.css';import CreateConsignment from './CreateConsignment';import Authenticate from './Authenticate';class App extends Component {state = {err: null,authenticated: false,}onAuth = (token) => {this.setState({authenticated: true,});}renderLogin = () => {return (<Authenticate onAuth={this.onAuth} />);}renderAuthenticated = () => {return (<CreateConsignment />);}getToken = () => {return localStorage.getItem('token') || false;}isAuthenticated = () => {return this.state.authenticated || this.getToken() || false;}render() {const authenticated = this.isAuthenticated();return (<div className="App"><div className="App-header"><h2>Shippy</h2></div><div className='App-intro container'>{(authenticated ? this.renderAuthenticated() : this.renderLogin())}</div></div>);}}export default App;```現在讓添加我們的兩個主要組件,Authenticate 和 CreateConsignment:```javascript// shippy-ui/src/Authenticate.javascriptimport React from 'react';class Authenticate extends React.Component {constructor(props) {super(props);}state = {authenticated: false,email: '',password: '',err: '',}login = () => {fetch(`http://localhost:8080/rpc`, {method: 'POST',headers: {'Content-Type': 'application/javascripton',},body: javascriptON.stringify({request: {email: this.state.email,password: this.state.password,},service: 'shippy.auth',method: 'Auth.Auth',}),}).then(res => res.javascripton()).then(res => {this.props.onAuth(res.token);this.setState({token: res.token,authenticated: true,});}).catch(err => this.setState({ err, authenticated: false, }));}signup = () => {fetch(`http://localhost:8080/rpc`, {method: 'POST',headers: {'Content-Type': 'application/javascripton',},body: javascriptON.stringify({request: {email: this.state.email,password: this.state.password,name: this.state.name,},method: 'Auth.Create',service: 'shippy.auth',}),}).then((res) => res.javascripton()).then((res) => {this.props.onAuth(res.token.token);this.setState({token: res.token.token,authenticated: true,});localStorage.setItem('token', res.token.token);}).catch(err => this.setState({ err, authenticated: false, }));}setEmail = e => {this.setState({email: e.target.value,});}setPassword = e => {this.setState({password: e.target.value,});}setName = e => {this.setState({name: e.target.value,});}render() {return (<div className='Authenticate'><div className='Login'><div className='form-group'><inputtype="email"onChange={this.setEmail}placeholder='E-Mail'className='form-control' /></div><div className='form-group'><inputtype="password"onChange={this.setPassword}placeholder='Password'className='form-control' /></div><button className='btn btn-primary' onClick={this.login}>Login</button><br /><br /></div><div className='Sign-up'><div className='form-group'><inputtype='input'onChange={this.setName}placeholder='Name'className='form-control' /></div><div className='form-group'><inputtype='email'onChange={this.setEmail}placeholder='E-Mail'className='form-control' /></div><div className='form-group'><inputtype='password'onChange={this.setPassword}placeholder='Password'className='form-control' /></div><button className='btn btn-primary' onClick={this.signup}>Sign-up</button></div></div>);}}export default Authenticate;```and...```javascript// shippy-ui/src/CreateConsignment.javascriptimport React from 'react';import _ from 'lodash';class CreateConsignment extends React.Component {constructor(props) {super(props);}state = {created: false,description: '',weight: 0,containers: [],consignments: [],}componentWillMount() {fetch(`http://localhost:8080/rpc`, {method: 'POST',headers: {'Content-Type': 'application/javascripton',},body: javascriptON.stringify({service: 'shippy.consignment',method: 'ConsignmentService.Get',request: {},})}).then(req => req.javascripton()).then((res) => {this.setState({consignments: res.consignments,});});}create = () => {const consignment = this.state;fetch(`http://localhost:8080/rpc`, {method: 'POST',headers: {'Content-Type': 'application/javascripton',},body: javascriptON.stringify({service: 'shippy.consignment',method: 'ConsignmentService.Create',request: _.omit(consignment, 'created', 'consignments'),}),}).then((res) => res.javascripton()).then((res) => {this.setState({created: res.created,consignments: [...this.state.consignments, consignment],});});}addContainer = e => {this.setState({containers: [...this.state.containers, e.target.value],});}setDescription = e => {this.setState({description: e.target.value,});}setWeight = e => {this.setState({weight: Number(e.target.value),});}render() {const { consignments, } = this.state;return (<div className='consignment-screen'><div className='consignment-form container'><br /><div className='form-group'><textarea onChange={this.setDescription} className='form-control' placeholder='Description'></textarea></div><div className='form-group'><input onChange={this.setWeight} type='number' placeholder='Weight' className='form-control' /></div><div className='form-control'>Add containers...</div><br /><button onClick={this.create} className='btn btn-primary'>Create</button><br /><hr /></div>{(consignments && consignments.length > 0? <div className='consignment-list'><h2>Consignments</h2>{consignments.map((item) => (<div><p>Vessel id: {item.vessel_id}</p><p>Consignment id: {item.id}</p><p>Description: {item.description}</p><p>Weight: {item.weight}</p><hr /></div>))}</div>: false)}</div>);}}export default CreateConsignment;```**注意**:我還將 Twitter Bootstrap 添加到 /public/index.html 並更改了一些CSS。現在運行使用者介面 `$ npm start`。 之後應該瀏覽器會自動開啟一個介面。您現在應該可以註冊並登入並查看 consignment 表單,您可以在其中建立新 consignments。看看你的開發工具中的 network 選項,然後看看 rpc 方法從我們的不同微服務中觸發和擷取我們的資料。第6部分到這裡就結束了,如果您有任何反饋,請給我發一封[電子郵件](ewan.valentine89@gmail.com),我會儘快回覆(有可能不會很及時,敬請見諒)。如果你發現這個系列有用,並且你使用了一個廣告攔截器。 請考慮為我的時間和努力贊助幾塊錢。十分感謝!! [https://monzo.me/ewanvalentine](https://monzo.me/ewanvalentine)
via: https://ewanvalentine.io/microservices-in-golang-part-6/
作者:André Carvalho 譯者:zhangyang9 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
701 次點擊 ∙ 1 贊