一个几乎没有前端开发经验的人,在两个月内尝试使用React+Redux+GAE/Go创建了一个Web应用的故事

总结

在这篇文章中,我将总结在两个月的时间里,我独自一人从零开始制作Web应用程序的经验和技巧。我的总结大致包括以下内容。

    • どんな流れでWebアプリを個人で作ってリリースまでもっていったか

 

    • その時にどういうことをしないといけないか

 

    • Reactでどうやってそこそこ本格的なWebアプリを作るか

 

    バックエンドとどんな感じで連携をしているのか

创建的东西(EveryChart)

这是一个能让任何人发表自己喜欢的口碑评价的网站。它基于GAE/Go进行运行,前端部分使用React + Redux + TypeScript(详细信息将在后文提及)。

【转发希望】我创建了一个名为“EveryChart”的网络服务,任何人都可以创建和分享自己喜欢的口碑评价!这是一个可以让任何人创建类似雷达图评价页面的服务。有兴趣的话,试试看吧~(^-^)/https://t.co/A4yVi96uPH #EveryChart pic.twitter.com/JQTQtZqsBM— branch@个人应用开发者 (@br_branch) August 31, 2019

everychart.gif

另外,源代码已在GitHub上公开。

undefined

※ セキュリティ上の理由でバックエンド側は中途半端な状態で公開してますが、トップページまでは動かせます。

在本文中,我们将讨论以下内容。

フロントエンド

React v16
Redux v4
TypeScript v3.5
Webpack v4
chart.js v2
Javascript Load Image v2

バックエンド

Google Cloud Platform (GCP)
GAE / Go1.9
Echo v3
Twitter API
Firestore API
Google Cloud Storage API

また、コマンドの実行環境は、Mac OSX(10.14.5) です。

请介绍一下自己(说明是什么样的人制作的)。

普段はJava or Goを使ったバックエンド開発をメインで行っています。
ただ、最近はグループマネージャーとして複数プロジェクトを見たりといった管理業務がメインになってます(プレイングマネージャーとして設計や実装などもそこそここなしてます)

フロントサイドは、7年前にはやってました。ただその頃はまだHTML4で、業務システムがメインだったのでリッチなこともしておらず、主にjQueryを使って作成をしていました。
というわけで最近のフロントエンド開発でいうとまったくの未経験者です。

我创建Web应用程序的原因

    • 最近のフロントエンドを勉強したかった

 

    • というかゼロからWeb開発したくなった

 

    いずれ独立・起業も視野にいれた副業を始めるためのポートフォリオになるものを作りたかった

というわけで、前置きが長くなりましたがここから先が本題になります。

What is the topic?

上記のように、勉強+ポートフォリオとなるものを作るというのが動機だったので、最初から 結構ガチなサイト を作ろうと考えてはいました。更には、作るときには以下の目標を立てながら開発をしました。

    • なるべく短期間で作る

 

    • ポートフォリオとして、バックエンド側は仕事のノウハウも活かせるものを作る

 

    • かつ、バックエンドでも初めてのことに挑戦する

 

    • 運用コストはほぼゼロにする

 

    毎日2時間だけ作業にあてる

結果としては、短期間かどうかはわかりませんが、2019/7/7に開始し、2018/8/31にリリースまで持っていけました。ちょうど8週間、約2ヶ月で、勉強しながらだったのでぼくなりには満足してます?

2ヶ月間のスケジュール

だいたい以下のスケジュール感で作成していきました。

    1. 何をつくるかとアーキテクチャなどもろもろ決める(2019/7/7:1日)

 

    1. 開発(バックエンド&フロントエンド)(2019/7/8〜2019/8/23:47日)

 

    1. 画像などのリソース作成 (開発と並行)

 

    1. ステージング環境での検証(2019/8/24〜2019/8/28:5日)

 

    1. 本番環境での最終確認(2019/8/29〜2019/8/30:2日)

 

    リリース(2019/8/31:1日)

何を作るか決める

最初にやったことは当然ながら何を作るかの検討でした。
とはいっても、以前から「こういうの欲しいなぁ」というアイデアはメモしたりしていて、そのネタ帳からひっぱってくるだけだったのですぐ決まりました。以前から「よく行くお店とか、感動した小説とかを自分なりにランク付けできるようなアプリがあるといいなぁ」って思ってたので、それを作ることにしました。

なるべく最小限の機能でリリース

大枠はきまったので、次はどういう機能を入れるかを検討します。平日の通勤時間を使って検討をしました。
こういうのを考えてる時が一番楽しいですね。
ただ、機能を入れすぎると短期間では作成ができないので、以下だけを作ることに。

アカウントの管理(Twitterでの認証)

TwitterAPIでの認証・認可
ログイン/ログアウト
登録と退会

評価ページをグループ化できる機能(ノート)

画像アップロードできる
説明文を書ける
編集と削除ができる

評価ページの作成

好きなチャートが作れる
チャートはアニメーションで表示される
画像アップロードできる
説明文を書ける
編集と削除ができる
評価ページにコメントができる
他の人も評価を投稿できる

その他細かい機能

利用規約とプライバシーポリシー
トップページの説明
全体セキュリティはしっかりと作り込む

だいたいざっくりと1つ3日(6時間)と見積もりました。そして重複してる機能は流用できると想定して、39日くらいでできるかなぁとか考え、更にはウォーズマン理論を応用することで7月中にリリースできるんじゃないかと考え、それを目標に開発を開始しました。

結果的には色んな要因でウォーズマン理論の応用は断念し、かつ最初の予定より2週間遅れちゃいましたが。
やっぱり、自分がウォーズマンではないと途中で気づいてしまったのが痛かったです。

何に挑戦するか

続いて、何に挑戦するかを決めます。これも念入りに検討したというよりは「前から興味はあったもの」を取り入れることにしました。
フロント側は、Reactを使う予定では最初からいました。Vue.jsやAnglarにも興味はあったのですが、Reactはライブラリが豊富って聞いてたのでそれで選んだ程度の感じです(次はVue.jsも使ってみたい)。
それで色々と調べていたら、最近では ReactとReduxを併用してState管理をするらしいということを知ったので、 React+Redux入門 とかを読みつつ、それも導入することにします。ついでにTypeScriptで書こうとも安易な気持ちで思い、以下の挑戦をすることにしました。

フロントエンド

React + Redux + TypeScriptでフロント側を作る
Webpack4を使ってみる

バックエンド

バックエンドは基本構成は今回は実績のあるもので(GAE/Go1.9)
ただFirestore Nativeモードを使ってみる
クリーンアーキテクチャとDDDにも挑戦してみる

架构

こんな感じの検討をしました。

全体構成.png

ディレクトリ構成

ディレクトリ構成はこんな感じです。
ただ、下の「反省点」でも書いたのですがフロント側のディレクトリ構成は失敗でした。
まあ、それも経験ということで採用した構成をそのまま載せます。
(反省点の箇所にこうしたらよかったというのは書いてます)

EveryChart
├── backend # バックエンド。最終的にはこの中のものがデプロイされる
│   ├── src # ソースディレクトリ
│   │   └── project # ルート
│   │       ├── core # サーバーの基本部分を扱ったパッケージ
│   │       ├── client # TwitterAPIやFirestoreなど、外部サービスと連携するためのクラスをまとめるパッケージ
│   │       |   ├── foon # Firestore APIを扱うパッケージ
│   │       |   ├── oauth # Twitter APIを扱うパッケージ
│   │       |   ├── session # Session API を扱うパッケージ
│   │       |   └── storage # Google Cloud Storage API を扱うパッケージ
│   │       ├── errors # errorを扱ったパッケージ
│   │       ├── handler # エンドポイントの処理をまとめたパッケージ
│   │       ├── mapper # JSONファイルとドメインモデルをマッピングするクラスを集めたパッケージ
│   │       ├── middlewares # サーバーの振る舞いを定義するクラスを集めたパッケージ
│   │       ├── model # ドメインモデルを集めたパッケージ
│   │       ├── persistence # データベースへ保管するEntityとレポジトリを定義したパッケージ
│   │       |   ├── data # データベースのEntityを表現したパッケージ (モデルのマッピングも行う)
│   │       |   └── repository # データアクセスを扱うパッケージ
│   │       ├── usecase # システムの振る舞いをまとめたパッケージ
│   │       ├── util # ユーティリティクラスをあつめたパッケージ
│   │       ├── vendor # depの依存パッケージが格納されるパッケージ
│   │       ├── Gopkg.toml # depの設定ファイル
│   │       └── main.go # バックエンド側のエントリポイント
│   ├── static # 静的ファイルの置き場
│   │   ├── images # faviconなどの画像置き場
│   │   └── js # webpackでコンパイルしたファイルが格納される
│   │       └── bundle.js # webpackの生成ファイル
│   ├── template # htmlのGo テンプレートを配置するディレクトリ
│   │   ├── layout # ベースとなるレイアウトを定義するパッケージ
│   │   ︙
│   ├── .envrc # direnvの設定ファイル
│   └── hogehoge.yaml # backendの設定ファイル。ローカル用/ステージング用/本番用を用意
└── front # フロントエンド側。この中のものがwebpackでコンパイルされ、 backend/static/js/bundle.js に格納される
    ├── node_modules # node.jsの依存パッケージが格納される
    ├── src # ソースファイルの配置場所
    │   ├── app # 各機能ごとのコンポーネントを表現。ただここのパッケージ構成は失敗だった。。
    │   │   ├── common # 共通のコンポーネントを表現
    │   │   ├── xxxx # 各機能
    │   │   │   ├── xxxComponent.tsx # UI部品
    │   │   │   ├── xxxActions.ts # イベントのアクションを定義
    │   │   │   ├── xxxContainer.ts # UIの振る舞いを定義
    │   │   │   └── xxxState.ts # UIの状態を定義
    │   │   ︙
    │   │   ├── component.tsx # 基礎とのあるレイアウト
    │   │   ├── routerActions.ts # 基本レイアウトのアクション
    │   │   ├── routerContainer.ts # 基本レイアウトのコンテナ
    │   │   └── routerState.ts # 基本レイアウトの状態を表したクラス
    │   ├── client # backendとの通信を行うクラスを扱ったパッケージ
    │   ├── model # クライアント側で扱うモデルを扱ったパッケージ
    │   ├── sample # 練習用のがそのまま残ってるだけ
    │   ├── types # 一部TypeScriptに対応していないライブラリの定義をするためのパッケージ
    │   ├── utils # ユーティリティクラス
    │   ├── consts.ts # 定数を集めたクラス
    │   ├── eventDispacher.ts # 共通で呼び出したいイベントを定義
    │   ├── index.css # 共通のCSS
    │   ├── index.tsx # フロント側のエントリポイント
    │   ├── registerServiceWorker.ts # service-worker.js を登録するためのスクリプト
    │   └── store.ts # ReduxのStore
    ├── package.json # パッケージ管理設定ファイル
    ├── tsconfig.json # TypeScriptの設定ファイル
    ├── tslint.json # TypeScriptの静的解析の定義ファイル
    └── webpack.config.js # Webpackの設定ファイル
処理の流れ.png

在实际情况下进行本地运行

実際の動きは、GitHubから落としてきて確認できます。
バックエンド側は公開しすぎちゃうとセキュリティ的にもよろしくないんで、コアとなる部分は消してしまっていますが、トップページまでなら見ることができるかと思います。

安装设置

将以下内容放入Mac中。

    • Go v1.9 (環境構築(Qiita))

 

    • Dep (環境構築(Qiita))

 

    • Direnv(環境構築(Qiita))

 

    • Gcloud(環境構築(Qiita))

 

    • Goapp (環境構築(Qiita))

 

    Firebase emurator (環境構築(Qiita))
# 上の必要なツール類は入れている状態
$ git clone https://github.com/brbranch/EveryChartSample --recursive
$ cd ./EveryChartSample/backend
$ direnv allow # direnvを有効にする
$ cd ./src/project
$ dep ensure # 依存ライブラリのDL
# firestore emuratorの起動
$ gcloud beta emulators firestore start --host-port=localhost:8915
# サーバーの起動(別タブなどでターミナルを開く)
$ goapp serve local.yaml

あとは、 http://127.0.0.1:8080 をブラウザから叩くとトップページが表示されると思います。

开始开发

因此,在考虑了组织结构等各个方面后,我们将开始开发。以下只列举重点。

后端开发

最开始是从熟悉的后端开始创建,而不是从前端开始。
构建方式一开始就按照上述的形式,先考虑模型并记录用例,然后通过Handler创建终端节点,接着编写进一步的处理程序。

使得可以通过Twitter进行认证

首先,由于无法登录,所以难以继续创建,因此选择通过Twitter关联进行创建。
从Twitter开发者平台注册应用程序,并获取应用程序的令牌和秘密信息。
(有关如何进行这些步骤的详细信息,请参阅https://qiita.com/kngsym2018/items/2524d21455aac111cdee)

补充一点,当使用Twitter的OAuth时,必须指定应用程序的回调URL。
然而,这个回调URL可以是 http://127.0.0.1:8080 或者其他任何合适的URL,所以请将其指定为 http://127.0.0.1:8080。

image.png

这部分的实现大致是这样的。

type AuthHandler struct {
}

func (a *AuthHandler) Handle(e *echo.Group) {
        // ... 中略...
    e.GET("/auth/twitter/:id", a.authTwitter)
}

func (a *AuthHandler) authTwitter(e echo.Context) error {
    ctx := core.NewContext(e)
    client := oauth.NewAuthClient(oauth.Twitter, ctx)
    requestUri, err := client.GetAuthUrl(e.Param("id"))

    if err != nil {
        return core.ErrorHTML(e, err)
    }

    return e.Redirect(http.StatusFound, requestUri)
}
type TwitterAuth struct {
    Context core.Context
}

type TwitterAccount struct {
    ID              string `json:"id_str"`
    Name            string `json:"name"`
    RefID           string `json:"screen_name"`
    Description     string `json:"description"`
    ProfileImageURL string `json:"profile_image_url_https"`
    Email           string `json:"email"`
}

func (t *TwitterAuth) Connect() *oauth.Client {
    return &oauth.Client{
        TemporaryCredentialRequestURI: "https://api.twitter.com/oauth/request_token",
        ResourceOwnerAuthorizationURI: "https://api.twitter.com/oauth/authenticate",
        TokenRequestURI:               "https://api.twitter.com/oauth/access_token",
        Credentials: oauth.Credentials{
            Token:  os.Getenv("TWITTER_AUTH_TOKEN"),
            Secret: os.Getenv("TWITTER_AUTH_SECRET"),
        },
    }
}

func (t *TwitterAuth) GetAuthUrl(anonymousId string) (string, error) {
    config := t.Connect()
    client := urlfetch.Client(t.Context)
    host := t.Context.Request().URL.Host
    url := fmt.Sprintf("https://%s/authc/twitter", schema, host)
    if os.Getenv("ENVIRONMENT") == "local" {
        url = "http://127.0.0.1:8080/authc/twitter"
    }
    rt, err := config.RequestTemporaryCredentials(client, url, nil)
    if err != nil {
        return "", t.Context.WrapErrorf(err, "failed to create request.")
    }
    sess, err := session.NewSession(t.Context)
    if err != nil {
        return "", t.Context.WrapErrorf(err, "failed to open session.")
    }

    sess.PutString(AnonymousIdSession, anonymousId)
    sess.PutString(twitterRequestToken, rt.Token)
    sess.PutString(twitterRequestSecret, rt.Secret)

    sess.Save()

    return config.AuthorizationURL(rt, nil), nil
}

これでTwitterに認可リクエストを投げるためのURLを作成しています。
そして、ユーザーが認可をし、指定したコールバック先に認証トークンが送られてきます。それが以下の場所です。

onst AuthSessionKey string = "AuthResultMessage"

type OAuthCallbackHandler struct {
}

func (a OAuthCallbackHandler) Handle(e *echo.Group) {
    e.GET("/twitter", a.authTwitter)
}

func (OAuthCallbackHandler) authTwitter(e echo.Context) error {
    ctx := core.NewContext(e)
    verifer := e.QueryParam("oauth_verifier")
    client := oauth.NewAuthClient(oauth.Twitter, ctx)
    account , err := client.GetAccount(verifer)

    if err != nil {
        return handleError(e, ctx, err)
    }

    service := usecase.NewLoginService(ctx)
    if ac , err := service.LoginOrSignup(account); err != nil {
        return handleError(e, ctx, err)
    } else {
        ctx.Infof("login (userID: %s)", ac.ID)
        return e.Redirect(http.StatusFound, "/home")
    }

    return e.Redirect(http.StatusFound, "/top")
}

func (t *TwitterAuth) GetAccount(verifier string) (*LinkedAccount, error) {
    token, err := t.GetAccessToken(verifier)
    if err != nil {
        return nil, err
    }
    oc := t.Connect()
    client := urlfetch.Client(t.Context)
    v := url.Values{}
    v.Set("include_email", "true")
    resp, err := oc.Get(client, token, "https://api.twitter.com/1.1/account/verify_credentials.json", v)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    if resp.StatusCode >= 500 {
        return nil, errors.New("Twitter is unavailable")
    }

    if resp.StatusCode >= 400 {
        return nil, errors.New("Twitter request is invalid")
    }

    twitter := &TwitterAccount{}
    err = json.NewDecoder(resp.Body).Decode(twitter)

    if err != nil {
        return nil, t.Context.WrapErrorf(err, "failed to decode account")
    }

    return &LinkedAccount{
        UniqueID:    fmt.Sprintf("twitter:%s", twitter.ID),
        Type:        "twitter",
        ID:          twitter.RefID,
        Name:        twitter.Name,
        ImageURL:    twitter.ProfileImageURL,
        Description: twitter.Description,
        Email:       twitter.Email,
    }, nil

}

func (t *TwitterAuth) GetAccessToken(verifier string) (*oauth.Credentials, error) {
    config := t.Connect()
    sess, err := session.NewSession(t.Context)
    if err != nil {
        return nil, t.Context.WrapErrorf(err, "failed to open session.")
    }

    token := sess.GetString(twitterRequestToken)
    secret := sess.GetString(twitterRequestSecret)

    if token == "" || secret == "" {
        return nil, exception.INVALID_SESSION
    }

    client := urlfetch.Client(t.Context)
    at, _, err := config.RequestToken(client, &oauth.Credentials{
        Token:  token,
        Secret: secret,
    }, verifier)

    defer func() {
        sess.Delete(twitterRequestSecret)
        sess.Delete(twitterRequestToken)
        sess.Save()
    }()


    if err != nil {
        return nil, t.Context.WrapErrorf(err, "failed to request token.")
    }

    sess.PutString(twitterOAuthSecret, at.Secret)
    sess.PutString(twitterOAuthToken, at.Token)
    return at, nil
}

然后,通过实际的登录/注册过程来将数据持久化,并在之后的身份认证中使用。

使用Go语言在Firestore的原生模式下调用API。

这次,数据永久化我们采用了Firestore。
Firestore原本是Firebase的一个服务,后来被纳入到GCP中,并计划在未来取代DataStore。它有DataStore兼容模式和原生模式两种。收费标准不会改变。

FirestoreとMemcacheを連携するライブラリがない

ただ、Firestoreのネイティブモードはランニングコストを抑えるという点ではひとつ欠点があり、Memcacheとの連携ライブラリがまだないというのがあります(どこかで、 Firestore自身がキャッシュする 、みたいな話も見たのですが、確認した限りは毎回取得のたびに無料枠を圧迫していってるような感じでした)。
DataStore互換モードだと、goonのようにMemcacheと連携してコストを抑えてくれるライブラリがあるんですけどね・・・。
そんなわけで、仕方ないので作成しました。

undefined

由于专注于制作这件事,花了很多时间,导致发布日期比计划延迟了(´・ω・`)。

客户端开发。

バックエンドはまあそんな感じで、続いてはクライアント側です。

创建入口点

まずはエントリポイントである index.tsxの作成を行います。
また、今回はReduxを使うので、storeへの登録といったものも行えるようにしておきます。
Reduxについては、こちらの記事が詳しいです。

Redux入門【ダイジェスト版】10分で理解するReduxの基礎
https://qiita.com/kitagawamac/items/49a1f03445b19cf407b7

ただ、Reduxをそのまま使うのは大変らしいので、今回はtypescript-fsaを使ってます。
このあたりは、以下を参考にしました。

在关东地区最快速地开发React+Redux+TypeScript应用程序。

// 
const history = createBrowserHistory()
export const store = createStore(
    Store(history),
    applyMiddleware(thunk, routerMiddleware(history))
)

// Material-UIテーマカスタマイズ
const theme = createMuiTheme({
    // 中略
});

// FontAwesomeの追加
library.add(fab, fas, far, faTwitter, faCoffee, faHeart, faComment, faCheckSquare, faExclamation, faExclamationCircle, faChartArea, faInfoCircle, faTrash, faUserLock, faFileAlt, faStar);

ReactDOM.render(
    // Storeの使用
    <Provider store={store}>
            // Material UIのテーマの設定
            <MuiThemeProvider theme={theme} >
                // ルーティングの設定
                <ConnectedRouter history={history}>
                    <Root/>
                </ConnectedRouter>
            </MuiThemeProvider>
        </Provider>,
    document.getElementById('app')
);

商店长这是这样的情况。


export type AppState = {
    login: LoginState,
    router: RouterState,
    // Root部分
    root: RootState,
    // 省略
};


export default (history: any) =>  combineReducers<AppState>({
    router: connectRouter(history),
    login: loginReducer,
    root: rootReducer,
    // 省略
    }
);

路由

使用react-router进行路由控制。
通过使用它,可以根据URL的不同显示不同的内容。

    return (
        <div className={classes.root}>
            <CssBaseline />
            <AppBar position="fixed" color="default" className={classes.appBar}>
            // 省略:共通ヘッダーの設定
            </AppBar>
            <main className={classes.content}>
                <div className={classes.toolbar} />
                {renderBody()}
                <div className={classes.hr}>&nbsp;</div>
                <div className={classes.footer}>
                    // 省略:共通フッターの設定
                </div>
            </main>
        // 省略: 通知ダイアログの設定
        </div>
    );

    function renderBody() {
        // エラーが存在してたら、他の描画はやめてエラーページを表示
        if (errorJson) {
            const err = JSON.parse(errorJson);
            return renderError(err.error);
        }
        // ルーティング
        return renderRouter();
    }

    function renderRouter() {
        return (
            // ルーティングの部分
            // pathに指定したパターンのコンポーネントのみを描画する
            <Switch>
                <Route exact path="/" render={({match}) => (<Login/>)} />
                <Route exact path="/top/terms" render={({match}) => (<Terms/>)} />
                <Route exact path="/top/policy" render={({match}) => (<PrivacyPolicy/>)} />
                <Route exact path="/top/info" render={({match}) => (<Information/>)} />
                // 省略
            </Switch>
        );
    }

随后的创作过程

在前台方面的工作中,按照以下流程进行了创建。

    • Actions / State / Container / Componentをそれぞれ作成する

Actionsにそのコンポーネントのアクションを定義
Stateで、アクションごとの状態変更を定義
Containerで、コンポーネントのふるまいを定義
ComponentでUI部品を定義

実際に取り扱うデータ部分はDataとModelに定義
ContainerまたはStateでModelを呼び出し、データの加工を行う

Containerで呼び出した場合は加工後のデータをActionで送る

Action / State / Container / Component の部分は、Reduxのやり方そのものかなって思います。
ただ、それだけだとちょっと使いづらかったので、バックエンドで表現してるモデルの一部はフロント側でも表現をしました。
(反省点で記載しますがそのやり方のデメリットもあったし、もっと効率の良いやり方もあったのかもしれませんが)

Redux含めた処理の流れ.png

Reducerというのは、Reduxの中でいい感じにアレしてくれるアレで、Stateのファイル内で各自定義してます。

以下に、例を一部抜粋します。

const actionCreator = actionCreatorFactory();
// Action
export const commentEditActions = {
    changeComment: actionCreator<string>('COMMENTEDIT_ACTION_CHNAGECOMMENT'),
};
// State
export interface NotebookCommentEditState {
    myComment: NotebookComment
    visible: boolean
    edit: boolean
}

// デフォルトのState(初期値)
const initialState: NotebookCommentEditState = {
    myComment: undefined,
    visible: false,
    edit: false,
};

// Reducer
export const notebookCommentEditReducer = reducerWithInitialState(initialState)
    .case(commentEditActions.changeComment, (state, value) => {
        const model = new NotebookCommentModel(state.myComment, null);
        model.changeComment(value);
        return {...state,edit:true, myComment: {...model.data()}}
    })
// データの定義
export interface NotebookComment {
    commentId: string
    // 省略
    hasComment: boolean,
    created: number
    isNew: boolean
}

export class NotebookCommentModel {
    private page: NotebookPage;
    private comment: NotebookComment;

    constructor(comment: NotebookComment, parent: NotebookPage) {
        this.comment = comment;
        this.page = parent;
    }

    changeComment(comment: string) {
        this.comment.comment = comment;
        this.comment.hasComment = this.hasComment();
    }

    data(): NotebookComment {
        return this.comment;
    }
}
// これはComponentから呼ばれるアクション(Event)
export interface Actions {
    changeComment: (value: string) => Action<string>;
}

// 上記のActionをPropsに変換してる
function mapDispatchToProps(dispatch: Dispatch) {
    return {
        // 上記のActionsの定義
        changeComment: (value: string) => {
            // ここで、Dispatchされる
            dispatch(commentEditActions.changeComment(value));
        }
    };
}

// stateをReactのPropsに変換してる
function mapStateToProps(appState: AppState) {
    return Object.assign({}, appState.notebookComment);
}

// 2つのプロパティをComponentに渡してる
export default connect(mapStateToProps, mapDispatchToProps)(Component);
// コンポーネント内の個別Styleを定義(Material-uiのメソッド)
const useStyles = makeStyles((theme: Theme) =>
    createStyles({
        notice : {
            color: "red",
            fontSize: "0.8em"
        }
    }),
);

// コンポーネントの独自プロパティを定義
interface Props {
    page: NotebookPage
}

// ActionsとNotebookCommentEditStateのプロパティをマージしてる
type OwnProps = Props & Actions & NotebookCommentEditState;

// FunctionalComponent
export const Component: React.FC<OwnProps> = (props: OwnProps) => {
    function showComment() {
        if(permission.hasCommentPermission()) {
            return (
                <Textarea title="コメント"
                          value={props.myComment.comment}
                          onChange={(v) => {
                              // イベントをContainerに送っている
                              props.changeComment(v);
                          }}/>
            )
        }
    }
    return (
        <Dialog
           // 省略
        >
            <DialogContent >
                {showComment()} // 上の関数呼び出し
            </DialogContent>
        </Dialog>
    );
}

从后端获取数据

在这个系统中,我们以两种方式从后端接收数据。

1. 在渲染时将JSON数据嵌入到script标签内

首先绘制屏幕的数据在渲染时作为JSON字符串嵌入到

bannerAds