apeescape2.com
  • メイン
  • Kpiと分析
  • 収益性と効率性
  • バックエンド
  • 財務プロセス
Webフロントエンド

GitHubWebhookを使用してWebアプリケーションを自動的にデプロイする

Webアプリケーションを開発し、それらを独自の管理されていないサーバーで実行しようとする人は誰でも、アプリケーションの展開と将来の更新のプッシュに伴う面倒なプロセスを認識しています。 Platform as a Service(PaaS)プロバイダーは、コストのわずかな増加と柔軟性の低下と引き換えに、個々のサーバーのプロビジョニングと構成のプロセスを経ることなく、Webアプリケーションを簡単に展開できるようにしました。 PaaSによって作業が簡単になった可能性がありますが、それでも、独自の管理されていないサーバーにアプリケーションをデプロイする必要がある、またはデプロイしたい場合があります。 Webアプリケーションをサーバーにデプロイするこのプロセスを自動化することは、最初は圧倒されるように聞こえるかもしれませんが、実際には、これを自動化するための簡単なツールを考え出すことは、思ったより簡単かもしれません。このツールの実装がどれほど簡単になるかは、ニーズがどれほど単純かによって大きく異なりますが、達成するのは確かに難しくはなく、面倒な繰り返しのWebアプリケーションを実行することで、多くの時間と労力を節約できる可能性があります。展開。

たくさんの 開発者 Webアプリケーションの展開プロセスを自動化する独自の方法を考え出しました。 Webアプリケーションのデプロイ方法は、使用されている正確なテクノロジースタックに大きく依存するため、これらの自動化ソリューションは互いに異なります。たとえば、自動的に含まれる手順 PHPWebサイトの展開 とは異なります Node.jsWebアプリケーションのデプロイ 。他のソリューションが存在します。 Dokku 、これはかなり一般的であり、これらのもの(ビルドパックと呼ばれる)は、より広範囲のテクノロジースタックでうまく機能します。

WebアプリケーションとWebhook



このチュートリアルでは、GitHub Webhook、ビルドパック、およびProcfilesを使用してWebアプリケーションのデプロイを自動化するためにビルドできるシンプルなツールの背後にある基本的なアイデアを見ていきます。この記事で探求するプロトタイププログラムのソースコードは次のとおりです。 GitHubで入手可能 。

Webアプリケーション入門

Webアプリケーションのデプロイを自動化するために、簡単なGoプログラムを作成します。 Goに慣れていない場合は、この記事全体で使用されているコード構造が非常に単純で理解しやすいものであるため、遠慮なくフォローしてください。気になる場合は、プログラム全体を選択した言語に簡単に移植できます。

開始する前に、Goディストリビューションがシステムにインストールされていることを確認してください。 Goをインストールするには、次の手順に従います。 公式ドキュメントに概説されている手順 。

次に、クローンを作成して、このツールのソースコードをダウンロードできます。 GitHubリポジトリ 。これにより、この記事のコードスニペットには対応するファイル名のラベルが付けられているため、簡単に理解できるはずです。必要に応じて、 やってみよう 直ちに。

このプログラムにGoを使用する主な利点の1つは、外部依存関係を最小限に抑えてプログラムを構築できることです。この場合、サーバーでこのプログラムを実行するには、GitとBashがインストールされていることを確認する必要があります。 Goプログラムは静的にリンクされたバイナリにコンパイルされるため、コンピューターでプログラムをコンパイルしてサーバーにアップロードし、ほとんど労力をかけずに実行できます。今日の他のほとんどの一般的な言語では、デプロイメントオートマターを実行するためだけに、サーバーに巨大なランタイム環境またはインタープリターをインストールする必要があります。 Goプログラムは、正しく実行されると、CPUで非常に簡単に実行できます。 羊 -これはあなたがこのようなプログラムに望むものです。

GitHub Webhook

GitHub Webhookを使用すると、リポジトリ内で何かが変更されたり、一部のユーザーがホストされたリポジトリで特定のアクションを実行したりするたびにイベントを発行するようにGitHubリポジトリを構成できます。これにより、ユーザーはこれらのイベントをサブスクライブし、リポジトリ周辺で発生するさまざまなイベントのURL呼び出しを通じて通知を受けることができます。

Webhookの作成は非常に簡単です。

新しいプログラミング言語を作成する
  1. リポジトリの設定ページに移動します
  2. 左側のナビゲーションメニューの[Webhooks&Services]をクリックします
  3. 「Webhookの追加」ボタンをクリックします
  4. URLを設定し、オプションでシークレット(受信者がペイロードを確認できるようにする)を設定します
  5. 必要に応じて、フォームで他の選択を行います
  6. 緑色の[Webhookを追加]ボタンをクリックしてフォームを送信します

Github Webhook

GitHubが提供する Webhookに関する広範なドキュメント そして、それらがどのように正確に機能するか、さまざまなイベントに応答してペイロードでどのような情報が配信されるかなどです。この記事の目的のために、私たちは特に 「プッシュ」イベント これは、誰かがリポジトリブランチにプッシュするたびに発行されます。

ビルドパック

最近のビルドパックはかなり標準的です。多くのPaaSプロバイダーで使用されているビルドパックを使用すると、アプリケーションをデプロイする前にスタックを構成する方法を指定できます。 Webアプリケーションのビルドパックを作成するのは非常に簡単ですが、多くの場合、Webをすばやく検索すると、変更を加えずにWebアプリケーションに使用できるビルドパックを見つけることができます。

HerokuのようなPaaSにアプリケーションをデプロイした場合、ビルドパックとは何か、そしてそれらをどこで見つけるかをすでに知っているかもしれません。 Herokuには包括的な機能があります ビルドパックの構造に関するドキュメント 、および よく構築された人気のビルドパックのリスト 。

自動化プログラムは、コンパイルスクリプトを使用して、アプリケーションを起動する前にアプリケーションを準備します。たとえば、HerokuによってビルドされたNode.jsは、package.jsonファイルを解析し、適切なバージョンのNode.jsをダウンロードして、アプリケーションのNPM依存関係をダウンロードします。物事を単純にするために、プロトタイププログラムではビルドパックを広範囲にサポートしないことに注意してください。今のところ、ビルドパックスクリプトはBashで実行するように記述されており、Ubuntuの新規インストールでそのまま実行されると想定します。必要に応じて、将来的にこれを簡単に拡張して、より難解なニーズに対応できます。

Procfiles

Procfileは、アプリケーションにあるさまざまなタイプのプロセスを定義できる単純なテキストファイルです。ほとんどの単純なアプリケーションでは、理想的には、HTTP要求を処理するプロセスである単一の「Web」プロセスがあります。

Procfilesの作成は簡単です。名前、コロン、プロセスを生成するコマンドを入力して、1行に1つのプロセスタイプを定義します。

ゲシュタルト知覚の5つの法則
:

たとえば、Node.jsベースのWebアプリケーションを使用している場合、Webサーバーを起動するには、コマンド「nodeindex.js」を実行します。コードのベースディレクトリにProcfileを作成し、次のように「Procfile」という名前を付けることができます。

web: node index.js

コードをプルした後に自動的に開始できるように、アプリケーションでProcfilesにプロセスタイプを定義する必要があります。

イベントの処理

プログラム内に、GitHubからの着信POSTリクエストを受信できるようにするHTTPサーバーを含める必要があります。 GitHubからのこれらのリクエストを処理するために、いくつかのURLパスを専用にする必要があります。これらの着信ペイロードを処理する関数は、次のようになります。

// hook.go type HookOptions struct { App *App Secret string } func NewHookHandler(o *HookOptions) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { evName := r.Header.Get('X-Github-Event') if evName != 'push' { log.Printf('Ignoring '%s' event', evName) return } body, err := ioutil.ReadAll(r.Body) if err != nil { http.Error(w, 'Internal Server Error', http.StatusInternalServerError) return } if o.Secret != '' { ok := false for _, sig := range strings.Fields(r.Header.Get('X-Hub-Signature')) { if !strings.HasPrefix(sig, 'sha1=') { continue } sig = strings.TrimPrefix(sig, 'sha1=') mac := hmac.New(sha1.New, []byte(o.Secret)) mac.Write(body) if sig == hex.EncodeToString(mac.Sum(nil)) { ok = true break } } if !ok { log.Printf('Ignoring '%s' event with incorrect signature', evName) return } } ev := github.PushEvent{} err = json.Unmarshal(body, &ev) if err != nil { log.Printf('Ignoring '%s' event with invalid payload', evName) http.Error(w, 'Bad Request', http.StatusBadRequest) return } if ev.Repo.FullName == nil || *ev.Repo.FullName != o.App.Repo { log.Printf('Ignoring '%s' event with incorrect repository name', evName) http.Error(w, 'Bad Request', http.StatusBadRequest) return } log.Printf('Handling '%s' event for %s', evName, o.App.Repo) err = o.App.Update() if err != nil { return } }) }

このペイロードを生成したイベントのタイプを確認することから始めます。 「プッシュ」イベントのみに関心があるため、他のすべてのイベントは無視できます。 「プッシュ」イベントのみを発行するようにWebhookを構成した場合でも、フックエンドポイントで受信することが期待できる他の種類のイベント「ping」が少なくとも1つあります。このイベントの目的は、GitHubでWebhookが正常に構成されているかどうかを判断することです。

次に、着信要求の本文全体を読み取り、Webhookの構成に使用するのと同じシークレットを使用してそのHMAC-SHA1を計算し、着信ペイロードの有効性を、ヘッダーに含まれている署名と比較して判断します。リクエスト。このプログラムでは、シークレットが構成されていない場合、この検証手順を無視します。ちなみに、ここで処理するデータの量に少なくとも何らかの上限を設けずに全身を読むことは賢明な考えではないかもしれませんが、重要な側面に焦点を当てるために物事を単純に保ちましょうこのツールの。

次に、からの構造体を使用します Go用のGitHubクライアントライブラリ 着信ペイロードをアンマーシャリングします。 「プッシュ」イベントであることがわかっているので、 PushEvent構造体 。次に、標準のjsonエンコーディングライブラリを使用して、ペイロードを構造体のインスタンスにマーシャリング解除します。いくつかの健全性チェックを実行し、問題がなければ、アプリケーションの更新を開始する関数を呼び出します。

アプリケーションの更新

Webhookエンドポイントでイベント通知を受信したら、アプリケーションの更新を開始できます。この記事では、このメカニズムのかなり単純な実装を見ていきます。確かに改善の余地があります。ただし、これは、基本的な自動展開プロセスを開始するための手段となるはずです。

webhookアプリケーションのフローチャート

ローカルリポジトリの初期化

このプロセスは、アプリケーションをデプロイするのが初めてかどうかを判断するための簡単なチェックから始まります。ローカルリポジトリディレクトリが存在するかどうかを確認することでこれを行います。存在しない場合は、最初にローカルリポジトリを初期化します。

// app.go func (a *App) initRepo() error { log.Print('Initializing repository') err := os.MkdirAll(a.repoDir, 0755) // Check err cmd := exec.Command('git', '--git-dir='+a.repoDir, 'init') cmd.Stderr = os.Stderr err = cmd.Run() // Check err cmd = exec.Command('git', '--git-dir='+a.repoDir, 'remote', 'add', 'origin', fmt.Sprintf(' [email protected] :%s.git', a.Repo)) cmd.Stderr = os.Stderr err = cmd.Run() // Check err return nil }

App構造体のこのメソッドは、ローカルリポジトリを初期化するために使用でき、そのメカニズムは非常に単純です。

  1. ローカルリポジトリが存在しない場合は、そのディレクトリを作成します。
  2. 「gitinit」コマンドを使用して、ベアリポジトリを作成します。
  3. リモートリポジトリのURLをローカルリポジトリに追加し、「origin」という名前を付けます。

初期化されたリポジトリがあれば、変更のフェッチは簡単です。

変更の取得

リモートリポジトリから変更をフェッチするには、次の1つのコマンドを呼び出す必要があります。

// app.go func (a *App) fetchChanges() error { log.Print('Fetching changes') cmd := exec.Command('git', '--git-dir='+a.repoDir, 'fetch', '-f', 'origin', 'master:master') cmd.Stderr = os.Stderr return cmd.Run() }

この方法でローカルリポジトリに対して「gitfetch」を実行することで、特定のシナリオでGitが早送りできないという問題を回避できます。強制フェッチは信頼できるものではありませんが、リモートリポジトリに強制プッシュする必要がある場合は、これで問題なく処理できます。

Linuxはどのライセンスで配布されていますか?

アプリケーションのコンパイル

ビルドパックのスクリプトを使用してデプロイ中のアプリケーションをコンパイルしているため、ここでのタスクは比較的簡単です。

// app.go func (a *App) compileApp() error { log.Print('Compiling application') _, err := os.Stat(a.appDir) if !os.IsNotExist(err) { err = os.RemoveAll(a.appDir) // Check err } err = os.MkdirAll(a.appDir, 0755) // Check err cmd := exec.Command('git', '--git-dir='+a.repoDir, '--work-tree='+a.appDir, 'checkout', '-f', 'master') cmd.Dir = a.appDir cmd.Stderr = os.Stderr err = cmd.Run() // Check err buildpackDir, err := filepath.Abs('buildpack') // Check err cmd = exec.Command('bash', filepath.Join(buildpackDir, 'bin', 'detect'), a.appDir) cmd.Dir = buildpackDir cmd.Stderr = os.Stderr err = cmd.Run() // Check err cacheDir, err := filepath.Abs('cache') // Check err err = os.MkdirAll(cacheDir, 0755) // Check err cmd = exec.Command('bash', filepath.Join(buildpackDir, 'bin', 'compile'), a.appDir, cacheDir) cmd.Dir = a.appDir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() }

最初に、以前のアプリケーションディレクトリ(存在する場合)を削除します。次に、新しいブランチを作成し、マスターブランチの内容をチェックアウトします。次に、構成済みのビルドパックの「検出」スクリプトを使用して、アプリケーションが処理できるものかどうかを判断します。次に、必要に応じて、ビルドパックのコンパイルプロセス用の「キャッシュ」ディレクトリを作成します。このディレクトリはビルド間で存続するため、以前のコンパイルプロセスで既に存在しているため、新しいディレクトリを作成する必要がない場合があります。この時点で、ビルドパックから「コンパイル」スクリプトを呼び出して、起動前にアプリケーションに必要なすべてのものを準備させることができます。ビルドパックが適切に実行されると、以前にキャッシュされたリソースのキャッシュと再利用を独自に処理できます。

アプリケーションの再起動

この自動展開プロセスの実装では、コンパイルプロセスを開始する前に古いプロセスを停止し、コンパイルフェーズが完了したら新しいプロセスを開始します。これによりツールの実装が容易になりますが、自動展開プロセスを改善するための驚くべき方法がいくつか残されています。このプロトタイプを改善するには、更新中のダウンタイムをゼロにすることから始めることができます。今のところ、より単純なアプローチを続けます。

戦略はビジネスモデルの設計です
// app.go func (a *App) stopProcs() error { log.Print('.. stopping processes') for _, n := range a.nodes { err := n.Stop() if err != nil { return err } } return nil } func (a *App) startProcs() error { log.Print('Starting processes') err := a.readProcfile() if err != nil { return err } for _, n := range a.nodes { err = n.Start() if err != nil { return err } } return nil }

プロトタイプでは、ノードの配列を反復処理することでさまざまなプロセスを停止および開始します。各ノードは、アプリケーションのインスタンスの1つに対応するプロセスです(サーバーでこのツールを起動する前に構成されています)。ツール内で、各ノードのプロセスの現在の状態を追跡します。また、それらの個別のログファイルも保持しています。すべてのノードが開始される前に、各ノードには、指定されたポート番号から始まる一意のポートが割り当てられます。

// node.go func NewNode(app *App, name string, no int, port int) (*Node, error) { logFile, err := os.OpenFile(filepath.Join(app.logsDir, fmt.Sprintf('%s.%d.txt', name, no)), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { return nil, err } n := &Node{ App: app, Name: name, No: no, Port: port, stateCh: make(chan NextState), logFile: logFile, } go func() { for { next := <-n.stateCh if n.State == next.State { if next.doneCh != nil { close(next.doneCh) } continue } switch next.State { case StateUp: log.Printf('Starting process %s.%d', n.Name, n.No) cmd := exec.Command('bash', '-c', 'for f in .profile.d/*; do source $f; done; '+n.Cmd) cmd.Env = append(cmd.Env, fmt.Sprintf('HOME=%s', n.App.appDir)) cmd.Env = append(cmd.Env, fmt.Sprintf('PORT=%d', n.Port)) cmd.Env = append(cmd.Env, n.App.Env...) cmd.Dir = n.App.appDir cmd.Stdout = n.logFile cmd.Stderr = n.logFile err := cmd.Start() if err != nil { log.Printf('Process %s.%d exited', n.Name, n.No) n.State = StateUp } else { n.Process = cmd.Process n.State = StateUp } if next.doneCh != nil { close(next.doneCh) } go func() { err := cmd.Wait() if err != nil { log.Printf('Process %s.%d exited', n.Name, n.No) n.stateCh <- NextState{ State: StateDown, } } }() case StateDown: log.Printf('Stopping process %s.%d', n.Name, n.No) if n.Process != nil { n.Process.Kill() n.Process = nil } n.State = StateDown if next.doneCh != nil { close(next.doneCh) } } } }() return n, nil } func (n *Node) Start() error { n.stateCh <- NextState{ State: StateUp, } return nil } func (n *Node) Stop() error { doneCh := make(chan int) n.stateCh <- NextState{ State: StateDown, doneCh: doneCh, } <-doneCh return nil }

一見すると、これはこれまでに行ったことよりも少し複雑に見えるかもしれません。わかりやすくするために、上記のコードを4つの部分に分けてみましょう。最初の2つは「NewNode」関数内にあります。呼び出されると、「ノード」構造体のインスタンスにデータが入力され、このノードに対応するプロセスの開始と停止に役立つGoルーチンが生成されます。他の2つは、「Node」構造体の2つのメソッド「Start」と「Stop」です。プロセスは、このノードごとのGoルーチンが監視している特定のチャネルを介して「メッセージ」を渡すことによって開始または停止されます。メッセージを渡してプロセスを開始するか、別のメッセージを渡してプロセスを停止することができます。プロセスの開始または停止に関連する実際のステップは単一のGoルーチンで行われるため、競合状態になる可能性はありません。

Goルーチンは、「stateCh」チャネルを介した「メッセージ」を待機する無限ループを開始します。このチャネルに渡されたメッセージがノードにプロセスの開始を要求する場合(「caseStateUp」内)、Bashを使用してコマンドを実行します。その間、ユーザー定義の環境変数を使用するようにコマンドを構成します。また、標準出力とエラーストリームを事前定義されたログファイルにリダイレクトします。

一方、プロセスを停止するには(「caseStateDown」内)、プロセスを強制終了します。これはおそらく創造性を発揮できる場所であり、プロセスを強制終了する代わりに、すぐにSIGTERMを送信し、数秒待ってから実際に強制終了して、プロセスを正常に停止する機会を与えます。

「Start」および「Stop」メソッドを使用すると、適切なメッセージをチャネルに簡単に渡すことができます。 「Start」メソッドとは異なり、「Stop」メソッドは実際にはプロセスが強制終了されるのを待ってから戻ります。 「開始」は、単にメッセージをチャネルに渡してプロセスを開始し、戻ります。

すべてを組み合わせる

最後に、私たちがする必要があるのは、プログラムのメイン機能内ですべてを配線することです。ここで、構成ファイルを読み込んで解析し、ビルドパックを更新し、アプリケーションの更新を1回試行し、Webサーバーを起動してGitHubからの着信「プッシュ」イベントペイロードをリッスンします。

// main.go func main() { cfg, err := toml.LoadFile('config.tml') catch(err) url, ok := cfg.Get('buildpack.url').(string) if !ok { log.Fatal('buildpack.url not defined') } err = UpdateBuildpack(url) catch(err) // Read configuration options into variables repo (string), env ([]string) and procs (map[string]int) // ... app, err := NewApp(repo, env, procs) catch(err) err = app.Update() catch(err) secret, _ := cfg.Get('hook.secret').(string) http.Handle('/hook', NewHookHandler(&HookOptions{ App: app, Secret: secret, })) addr, ok := cfg.Get('core.addr').(string) if !ok { log.Fatal('core.addr not defined') } err = http.ListenAndServe(addr, nil) catch(err) }

ビルドパックは単純なGitリポジトリである必要があるため、「UpdateBuildpack」( buildpack.go )ビルドパックのローカルコピーを更新するために、必要に応じてリポジトリURLで「gitclone」と「gitpull」を実行するだけです。

試してみる

クローンを作成していない場合 レポジトリ それでも、あなたは今それをすることができます。 Goディストリビューションがインストールされている場合は、プログラムをすぐにコンパイルできるはずです。

php restapiサンプルコード
mkdir hopper cd hopper export GOPATH=`pwd` go get github.com/hjr265/toptal-hopper go install github.com/hjr265/toptal-hopper

この一連のコマンドは、hopperという名前のディレクトリを作成し、それをGOPATHとして設定し、必要なGoライブラリとともにGitHubからコードをフェッチし、プログラムを「$ GOPATH / bin」ディレクトリにあるバイナリにコンパイルします。これをサーバーで使用する前に、これをテストするための簡単なWebアプリケーションを作成する必要があります。便宜上、Node.js Webアプリケーションのような単純な「Hello、world」を作成してアップロードしました。 別のGitHubリポジトリ これをフォークして、このテストに再利用できます。次に、コンパイルされたバイナリをサーバーにアップロードし、同じディレクトリに構成ファイルを作成する必要があります。

# config.tml [core] addr = ':26590' [buildpack] url = 'https://github.com/heroku/heroku-buildpack-nodejs.git' [app] repo = 'hjr265/hopper-hello.js' [app.env] GREETING = 'Hello' [app.procs] web = 1 [hook] secret = ''

構成ファイルの最初のオプションである「core.addr」は、プログラムの内部WebサーバーのHTTPポートを構成できるようにするものです。上記の例では、「:26590」に設定しています。これにより、プログラムは「http:// {host}:26590 / hook」で「プッシュ」イベントペイロードをリッスンします。 GitHub Webhookを設定するときは、「{host}」をサーバーを指すドメイン名またはIPアドレスに置き換えるだけです。ある種のファイアウォールを使用している場合に備えて、ポートが開いていることを確認してください。

次に、GitURLを設定してビルドパックを選択します。ここでは使用しています HerokuのNode.jsビルドパック 。

「app」の下で、「repo」をアプリケーションコードをホストするGitHubリポジトリのフルネームに設定します。サンプルアプリケーションを「https://github.com/hjr265/hopper-hello.js」でホストしているため、リポジトリのフルネームは「hjr265 /hopper-hello.js」です。

次に、アプリケーションのいくつかの環境変数とそれぞれの数を設定します プロセスの種類 必要です。そして最後に、着信「プッシュ」イベントペイロードを検証できるようにシークレットを選択します。

これで、サーバー上で自動化プログラムを開始できます。すべてが正しく構成されている場合(サーバーからリポジトリにアクセスできるようにSSHキーをデプロイするなど)、プログラムはコードをフェッチし、ビルドパックを使用して環境を準備し、アプリケーションを起動する必要があります。これで、GitHubリポジトリにWebhookを設定して、プッシュイベントを発行し、「http:// {host}:26590 / hook」を指すようにするだけです。 「{host}」は、サーバーを指すドメイン名またはIPアドレスに置き換えてください。

ついに テスト サンプルアプリケーションにいくつかの変更を加えて、GitHubにプッシュします。自動化ツールがすぐに動作し、サーバー上のリポジトリを更新し、アプリケーションをコンパイルして再起動することに気付くでしょう。

結論

私たちの経験のほとんどから、これは非常に便利なものであることがわかります。この記事で用意したプロトタイプアプリケーションは、本番システムでそのまま使用したいものではない場合があります。改善の余地はたくさんあります。このようなツールは、エラー処理が改善され、正常なシャットダウン/再起動をサポートする必要があります。プロセスを直接実行するのではなく、Dockerなどを使用してプロセスを含めることができます。特定のケースに何が必要かを正確に把握し、そのための自動化プログラムを考え出す方が賢明かもしれません。または、インターネット全体で利用できる、他のはるかに安定した、実績のあるソリューションを使用することもできます。しかし、非常にカスタマイズされたものを展開したい場合は、この記事がそれを実行するのに役立ち、Webアプリケーションの展開プロセスを自動化することで長期的にどれだけの時間と労力を節約できるかを示すことを願っています。

関連: 強化されたGitフローの説明

ReactフックとTypeScriptの操作

技術

ReactフックとTypeScriptの操作
クールな滞在:設計フィードバックを戦略的に行う方法

クールな滞在:設計フィードバックを戦略的に行う方法

Uxデザイン

人気の投稿
デザインディーバを管理する方法(そして1つではない)
デザインディーバを管理する方法(そして1つではない)
デザインのパートナー–クライアントの共感へのガイド
デザインのパートナー–クライアントの共感へのガイド
収益の質:財務デューデリジェンスの重要な柱
収益の質:財務デューデリジェンスの重要な柱
デザイントーク:UXリサーチャーのCaitriaO'Neillとの実際の研究
デザイントーク:UXリサーチャーのCaitriaO'Neillとの実際の研究
野生のRailsエンジンのガイド:実際のRailsエンジンの実例
野生のRailsエンジンのガイド:実際のRailsエンジンの実例
 
フレームワークを保持する–依存性注入パターンの調査
フレームワークを保持する–依存性注入パターンの調査
ブランドマーケティング担当副社長
ブランドマーケティング担当副社長
SRVB暗号システム入門
SRVB暗号システム入門
デジタル遊牧民のための人間工学:自殺せずに道路で働く
デジタル遊牧民のための人間工学:自殺せずに道路で働く
ビデオゲームの物理チュートリアル-パートIII:拘束された剛体シミュレーション
ビデオゲームの物理チュートリアル-パートIII:拘束された剛体シミュレーション
人気の投稿
  • PythonでTwitterデータをマイニング
  • 勘定科目表の作り方
  • SQLServerのパフォーマンスチューニングのヒント
  • nodejsはすべてのブラウザで機能しますか
  • scorpとaccorpの違い
  • デザイン重視の原則
  • クレジットカードの詳細をハックする2018
カテゴリー
その他 リモートの台頭 モバイルデザイン バックエンド 分散チーム 革新 データサイエンスとデータベース Uiデザイン Uxデザイン 仕事の未来

© 2021 | 全著作権所有

apeescape2.com