UIでチケットを恐れるこずをやめた方法

みなさんこんにちは。
開発でReactJSを䜿甚し始めおから1幎以䞊が経過したした。 最埌に、私たちの䌚瀟がどれほど幞せになったかを共有する時が来たした。 この蚘事では、このラむブラリを䜿甚するようになった理由ずその方法に぀いお説明したす。

なぜこれすべお


私たちは小さな䌚瀟です。スタッフは玄50人で、そのうち20人は開発者です。 珟圚、4぀の開発チヌムがあり、それぞれに5人のフルスタック開発者がいたす。 しかし、自分自身をフルスタックの開発者ず呌ぶこずず、SQL Serverの䜜業、ASP.NET、Cでの開発、OOP、DDD、HTML、CSS、JSを熟知し、すべおを賢く䜿甚できるこずを理解するこずは本圓に良いこずです。 もちろん、各開発者は異なるものに匕き寄せられたすが、私たち党員が䜕らかの方法で.NET開発の専門家であり、コヌドの90はCで蚘述されおいたす。
圓瀟の補品-マヌケティング自動化システム-は、特定のクラむアントごずに倧量の蚭定を必芁ずしたす。 マネヌゞャヌが顧客向けに補品をカスタマむズできるようにするために、郵送を開始したり、トリガヌやその他のメカニズムを䜜成したり、サヌビスをカスタマむズしたりするこずができる管理サむトがありたす。 この管理サむトには倚くのさたざたな非自明なUIが含たれおおり、カスタマむズするポむントが现かくなればなるほど、運甚環境でリリヌスする機胜が増えるほど、UIは興味深いものになりたす。

トリガヌ䜜成


補品カテゎリで絞り蟌む


以前、このようなUIの開発にどのように察凊したしたか うたく察凊できたせんでした。 基本的に、Ajaxを受信したHTMLの䞀郚をサヌバヌ䞊でレンダリングするこずに成功したした。 たたは、jQueryを䜿甚したむベントでのみ。 ナヌザヌにずっお、これは通垞、継続的なダりンロヌド、くしゃみごずのプリロヌダヌ、奇劙なバグをもたらしたした。 開発者の芳点から芋るず、これらは誰もが恐れおいた本圓のパスタでした。 蚈画のUIのチケットはすべお、Lの芋積もりをすぐに受け取り、コヌドを蚘述するずきに倧量のボタンホヌルに泚がれたした。 そしお、もちろん、そのようなUIに関連する倚くのバグがありたした。 これは次のように起こりたした。最初の実装では、いく぀かの小さな間違いが行われたした。 そしお、この奇跡のテストがなかったため、他の䜕かを修埩するずき、必然的にバラバラになりたした。
人生からの䟋。 これが操䜜䜜成ペヌゞです。 ビゞネスに぀いお詳しく説明しなくおも、私たちずの操䜜は、クラむアントの請負業者が䜿甚できるRESTサヌビスのようなものだずしか蚀えたせん。 この操䜜では、消費者登録の段階に応じお可甚性に制限があり、構成するために次のような制埡がありたした。
䜜成操䜜


そしお、このコントロヌルの叀いコヌドは次のずおりです。
操䜜可甚性衚瀺制埡コヌド
ビュヌのスラむス
<h2 class="column-header"> <span class="link-action" data-event-name="ToggleElements" data-event-param='{"selector":"#WorkFlowAllowance", "callback": "toggleWorkflowAvailability"}'>     </span> </h2> @Html.HiddenFor(m => m.IsAllowedForAllWorkflow, new { Id = "IsAllowedForAllWorkflow" }) <div id="WorkFlowAllowance" class="@(Model.IsAllowedForAllWorkflow ? "none" : string.Empty) row form_horizontal"> <table class="table table_hover table_control @(Model.OperationWorkflowAllowances.Any() ? String.Empty : "none")" id="operationAllowanceTable"> <thead> <tr> <th> </th> <th></th> </tr> </thead> <tbody> @Model.OperationWorkflowAllowances.Each( @<tr> <td> @item.Item.WorkflowDisplayName <input type="hidden" name="OperationTypeViewModel.OperationWorkflowAllowances[@(item.Index)].WorkflowName" value="@item.Item.WorkflowName" /> <input type="hidden" name="OperationTypeViewModel.OperationWorkflowAllowances[@(item.Index)].WorkflowDisplayName" value="@item.Item.WorkflowDisplayName" /> <input type="hidden" name="OperationTypeViewModel.OperationWorkflowAllowances[@(item.Index)].Id" value="@item.Item.Id" /> </td> <td> <button class="cell-grid__right button button_icon-only button_red removeOperationAllowance"><span class="icon icon_del"></span></button> <span class="cell-grid__wraps">@(item.Item.StageName ?? "")</span> <input type="hidden" name="OperationTypeViewModel.OperationWorkflowAllowances[@(item.Index)].StageName" value="@item.Item.StageName" /> <input type="hidden" name="OperationTypeViewModel.OperationWorkflowAllowances[@(item.Index)].StageDisplayName" value="@item.Item.StageDisplayName" /> </td> </tr>) </tbody> </table> <div class="col col_462"> <div class="form-group form-group_all"> </div> @if (Model.WorkFlows.Any()) { <div> <div class="form-group"> <label class="form-label"><span> </span></label> @Html.DropDownList("WorkflowList", Model.WorkFlows, new Dictionary<string, object> { { "class", "form-control select2 w470" }, { "data-placeholder", "  " }, { "id", "workflowList" }, { "disabled", "disabled" } }) </div> <div class="form-group"> <div class="form-list"> <input id="isAllowedForAllStagesForCurrentWorkflow" type="checkbox" name="StageMechanicsRegistratioName" autocomplete="off"> <label for="isAllowedForAllStagesForCurrentWorkflow">     <span id="exceptAnonymus"></span><span id="workflowName"></span></label> </div> </div> <div class="form-group"> <label class="form-label"><span></span></label> @Html.DropDownList("WorkflowStageList", new SelectListItem[0], new Dictionary<string, object> { { "class", "form-control select2 w470" }, { "data-placeholder", "  " }, { "id", "workflowStageList" }, { "disabled", "disabled"} }) </div> <div class="form-group"> <button class="button button_blue" id="addOperationAllowance"> </button> </div> </div> } else { @:     } </div> </div> 

そしお、このビュヌを機胜させたjsは次のずおりです実行可胜なコヌドを衚瀺するこずを目的ずしおいたのではなく、それがどれほど悲しかったかを瀺しおいたす。
 function initOperationAllowance(typeSelector) { $('#workflowList').prop('disabled', false); $('#workflowList').trigger('change'); if ($(typeSelector).val() == 'PerformAction') { $('#exceptAnonymus').html('( )'); } else { $('#exceptAnonymus').html(''); } } function toggleWorkflowAvailability() { var element = $("#IsAllowedForAllWorkflow"); $('#operationAllowanceTable tbody tr').remove(); parameters.selectedAllowances = []; return element.val().toLowerCase() == 'true' ? element.val(false) : element.val(true); } function deleteRow(row) { var index = getRowIndex(row); row.remove(); parameters.selectedAllowances.splice(index, 1); $('#operationAllowanceTable input').each(function () { var currentIndex = getFieldIndex($(this)); if (currentIndex > index) { decrementIndex($(this), currentIndex); } }); if (parameters.selectedAllowances.length == 0) { $('#operationAllowanceTable').hide(); } } function updateWorkflowSteps(operationType) { var workflow = $('#workflowList').val(); if (workflow == '') { $('#isAllowedForAllStagesForCurrentWorkflow') .prop('checked', false) .prop('disabled', 'disabled'); refreshOptionList( $('#workflowStageList'), [{ Text: '  ', Value: '', Selected: true }] ); $('#workflowStageList').trigger('change').select2('enable', false); return; } var url = parameters.stagesUrlTemplate + '?workflowName=' + workflow + '&OperationTypeName=' + operationType; $.getJSON(url, null, function (data) { $('#isAllowedForAllStagesForCurrentWorkflow') .prop('checked', false) .removeProp('disabled'); refreshOptionList($('#workflowStageList'), data); $('#workflowStageList').trigger('change').select2('enable', true); }); } function refreshOptionList(list, data) { list.find('option').remove(); $.each(data, function (index, itemData) { var option = new Option(itemData.Text, itemData.Value, null, itemData.Selected); list[0].add(option); }); } function AddRow(data) { var rowsCount = $('#operationAllowanceTable tr').length; var index = rowsCount - 1; var result = '<tr ' + (rowsCount % 2 != 0 ? 'class="bgGray">' : '>') + '<td>' + '{DisplayWorkflowName}' + '<input type="hidden" name="OperationTypeViewModel.OperationWorkflowAllowances[' + index + '].WorkflowName" value="{WorkflowName}"/>' + '<input type="hidden" name="OperationTypeViewModel.OperationWorkflowAllowances[' + index + '].Id" value=""/>' + '<input type="hidden" name="OperationTypeViewModel.OperationWorkflowAllowances[' + index + '].WorkflowDisplayName" value="{DisplayWorkflowName}"/>' + '</td>' + '<td>' + '<button class="cell-grid__right button button_icon-small button_red removeOperationAllowance"><span class="icon icon_del"></span></button>' + '<span class="cell-grid__wraps">{DisplayStageName}</span>' + '<input type="hidden" name="OperationTypeViewModel.OperationWorkflowAllowances[' + index + '].StageName" value="{StageName}"/>' + '<input type="hidden" name="OperationTypeViewModel.OperationWorkflowAllowances[' + index + '].StageDisplayName" value="{DisplayStageName}"/>' + '</td>' + '</tr>'; for (key in data) { result = result.replace(new RegExp('{' + key + '}', 'g'), data[key]); } $('#operationAllowanceTable').show().append(result); } function IsValidForm() { var result = ValidateList($('#workflowList'), '    ') & ValidateListWithCheckBox($('#workflowStageList'), $('#isAllowedForAllStagesForCurrentWorkflow'), '     '); if (!result) return false; var workflowName = $('#workflowList').val(); var stageName = ''; if (!$('#isAllowedForAllStagesForCurrentWorkflow').is(':checked')) { stageName = $('#workflowStageList').val(); } hideError($('#workflowList')); hideError($('#workflowStageList')); for (var i = 0; i < parameters.selectedAllowances.length; i++) { if (parameters.selectedAllowances[i].workflow == workflowName && parameters.selectedAllowances[i].stage == stageName) { if (stageName == '') { showError($('#workflowList'), '      '); } else { showError($('#workflowStageList'), '     '); } result = false; } else if (parameters.selectedAllowances[i].workflow == workflowName && parameters.selectedAllowances[i].stage == '') { showError($('#workflowList'), '      '); result = false; } } return result; } function ValidateList(field, message) { if (field.val() == "") { showError(field, message); return false; } hideError(field); return true; } function ValidateListWithCheckBox(field, checkBoxField, message) { if (!checkBoxField.prop('checked')) { return ValidateList(field, message); } hideError(field); return true; } function showError(field, message) { if (typeof (message) === 'undefined') { message = '   '; } field.addClass('input-validation-error form-control_error'); field.parent('.form-group').find('div.tooltip-error').remove(); field.closest('.form-group').append( '<div class="tooltip-icon tooltip-icon_error"><div class="tooltip-icon__content">' + '<strong></strong><br>' + message + '</div></div>'); } function hideError(field) { field.removeClass('input-validation-error form-control_error'); field.parent('.form-group').find('div.tooltip-icon_error').remove(); } function getRowIndex(row) { return getFieldIndex(row.find('input:first')); } function getFieldIndex(field) { var name = field.prop('name'); var startIndex = name.indexOf('[') + 1; var endIndex = name.indexOf(']'); return name.substr(startIndex, endIndex - startIndex); } function decrementIndex(field, index) { var name = field.prop('name'); var newIndex = index - 1; field.prop('name', name.replace('[' + index + ']', '[' + newIndex + ']')); } function InitializeWorkflowAllowance(settings) { $(function() { parameters.selectedAllowances = settings.selectedAllowances; initOperationAllowance(parameters.typeSelector); $('#workflowList').change(function () { updateWorkflowSteps($(parameters.typeSelector).val()); }); $('#addOperationAllowance').click(function (event) { event.preventDefault(); if (IsValidForm()) { var data = { 'StageName': $('#workflowStageList').val(), 'WorkflowName': $('#workflowList').val(), }; if ($('#isAllowedForAllStagesForCurrentWorkflow').is(':checked')) { data.DisplayWorkflowName = $('#workflowList option[value=' + data.WorkflowName + ']').text(); data.DisplayStageName = ''; data.StageName = ''; } else { data.DisplayWorkflowName = $('#workflowList option[value=' + data.WorkflowName + ']').text(); data.DisplayStageName = $('#workflowStageList option[value=' + data.StageName + ']').text(); } AddRow(data); if (data.StageName == '') { var indexes = []; //      for (var i = 0; i < parameters.selectedAllowances.length; i++) { if (parameters.selectedAllowances[i].workflow == data.WorkflowName) { indexes.push(i); } } $("#operationAllowanceTable tbody tr").filter(function (index) { return $.inArray(index, indexes) > -1; }).each(function () { deleteRow($(this)); }); } parameters.selectedAllowances.push({ workflow: data.WorkflowName, stage: data.StageName }); $("#workflowList").val('').trigger('change'); updateWorkflowSteps($(parameters.typeSelector).val()); } }); $('#isAllowedForAllStagesForCurrentWorkflow').click(function () { if ($(this).is(":checked")) { $('#workflowStageList').prop('disabled', 'disabled'); } else { $('#workflowStageList').removeProp('disabled'); } }); $('#operationAllowanceTable').on('click', 'button.removeOperationAllowance', function (event) { var row = $(this).parent().parent(); setTimeout(function () { deleteRow(row); }, 20); event.preventDefault(); }); }); 



新しい垌望


ある時点で、そのように生きるこずはもはや䞍可胜であるこずに気づきたした。 いく぀かの議論の埌、フロント゚ンドを理解し、真の道を指瀺する偎の人が必芁であるずいう結論に達したした。 Reactの䜿甚を提案するフリヌランサヌを雇いたした。 圌は私たちずあたり仕事をしおいたせんでしたが、䜕が起こっおいるのかを瀺すためにいく぀かのコントロヌルを䜜成するこずができたした。 公匏りェブサむトのチュヌトリアルを完了しおからReactが本圓に奜きでしたが、誰もがそれを気に入りたせんでした。 さらに、筋金入りのフロント゚ンド開発者はjavascriptを愛しおいたすが、静的な型の開発の䞖界では、javascriptは軜床に蚀えば人気がないため、䜿甚するように提䟛されたこれらのWebパックやうなり声はすべお怖がっおいたす。 その結果、察凊する必芁があるフレヌムワヌクを決定するために、異なるフレヌムワヌクを䜿甚しお、耇雑なUIのいく぀かのプロトタむプを䜜成するこずが決定されたした。 遞択した各フレヌムワヌクのサポヌタヌは、コヌドを比范できるように同じコントロヌルのプロトタむプを䜜成する必芁がありたした。 Angular、React、Knockoutを比范したした。 埌者はプロトタむプ段階を経るこずさえありたせんでしたし、私はどんな理由で芚えおいるこずすらありたせん。 しかし、AngularずReactの支持者の間で、䌚瀟は真の内戊を開始したした
冗談:)実際、各フレヌムワヌクには1人のサポヌタヌがいたしたが、他の誰もがどちらも奜きではありたせんでした。 誰もがためらい、䜕も決められなかった。 Angularでは、誰もがその耇雑さに悩たされ、Reactでは、その圓時のVisual Studioでのサポヌトの欠劂が本圓に非垞に䞍愉快な事実であった、愚かな構文でした。
幞いなこずに、私たちの䞊叞䌚瀟の所有者の1人が私たちを助けおくれたした。もちろん、長い間プログラムしおいたせんでしたが、圌の指は脈動を保っおいたす。 プロトタむプが効果をもたらさないこずが明らかになり、開発が䜕らかの理由で時間を浪費するようになった埌その時点で、比范のためのコヌドを増やすために、さらに倧きなサむズの別のプロトタむプを䜜成する蚈画を立おおいたした、圌。 さお、なぜ圌の遞択がただReactに萜ちたかを思い出しお、Sasha agornik Gornikは私に次のように蚀った私はホリバヌのためではなく圌の蚀葉を匕甚する、これは単なる意芋である。 
いく぀かのプロトタむプがありたした反応、角床、および他のもの。 芋たした。 私は角が奜きではなく、反応が奜きでした。
しかし、[倧声で]倧声で叫び、他のみんなは野菜のようでした。 読んで芋なければなりたせんでした。
この反応は、倚くのクヌルなサむトで生産されおいるこずがわかりたした。 FB、Yahoo、WhatsAppなどがありたす。 明らかに巚倧な採甚が来おおり、未来がありたす。
そしお栌玍庫で-[䜕も良いこずはありたせん]。 未来を芋たした。 2.0で匷化したいアングルのプロトタむプで、私が奜たなかったものはすべお芋たした。
反応は人生のために䜜られたものであり、特定の問題を解決するものであるこずに気づいた そしお角床-Googleの脳からの理論家はあごひげを生やし、あらゆる皮類の抂念を思い぀きたす。 GWTたたはそれが䜕であれ、そうでした。
たあ、私は匷い意志で野菜の味方をする必芁があるこずに気づきたした、さもなければ掟手で間違ったものが勝぀でしょう。 これを行う前に、3,300䞇の蚌拠ずリンクをチャンネルに投げ蟌み、[チヌフアヌキテクト]の支揎を求め、誰も倢䞭にならないように努めたした。
たた、地獄のような重芁な議論を思い出したした。 反応のために、それを段階的に実行しお既存のペヌゞにねじ蟌む矎しい方法ず、それらを完党にやり盎すのに必芁な角床があり、これも[貧しい]アヌキテクチャで修正されたす。
それから私はたた、反応ずしお、理論的には、UIがWebに察しおも実行できるこずを読みたした。 そしお、すべおのサヌバヌ偎のjs /そこに反応し、それがすべお行く堎所。 そしお最埌に、あなたは単䞀の議論を取るこずができたせんでした。
スタゞオのサポヌトはすぐに削枛されるこずに気付きたした。 最終的に、すべおがたったく同じように起こりたした。 私は確かにこの決定にずおもうれしいです

どうしたの


カヌドを公開し、UIをどのように調理しおいるかを瀺したす。 もちろん、フロント゚ンドのアヌティストは今すぐ笑い始めたすが、私たちにずっおこのコヌドは本圓の勝利であり、ずおも満足しおいたす:)
たずえば、远加のフィヌルドを䜜成するためにペヌゞを䜿甚したす。 簡単なビゞネス参照消費者、泚文、賌入、補品などの䞀郚の゚ンティティには、顧客固有のデヌタが関連付けられおいる堎合がありたす。 そのようなデヌタを保存するために、埓来の゚ンティティ-属性-倀モデルを䜿甚したす 。 最初は、各クラむアントの远加フィヌルドが開発時間を節玄するためにデヌタベヌスに盎接入力されたしたが、最埌に、UIの時間も芋぀かりたした。
プロゞェクトにフィヌルドを远加するペヌゞは次のずおりです。
列挙型のフィヌルドを远加する


String型のフィヌルドを远加する


そしお、React䞊でこのペヌゞのコヌドは次のようになりたす。
远加のフィヌルドを远加/線集するためのペヌゞのコンポヌネント
 /// <reference path="../../references.d.ts"/> module DirectCrm { export interface SaveCustomFieldKindComponentProps extends Model<CustomFieldKindValueBackendViewModel> { } interface SaveCustomFieldKindComponentState { model?: CustomFieldKindValueBackendViewModel; validationContext: IValidationContext<CustomFieldKindValueBackendViewModel>; } export class SaveCustomFieldKindComponent extends React.Component<SaveCustomFieldKindComponentProps, SaveCustomFieldKindComponentState> { private _componentsMap: ComponentsMap<CustomFieldKindConstantComponentDataBase, CustomFieldKindTypedComponentProps>; constructor(props: SaveCustomFieldKindComponentProps) { super(props); this.state = { model: props.model, validationContext: createTypedValidationContext<CustomFieldKindValueBackendViewModel>(props.validationSummary) }; this._componentsMap = ComponentsMap.initialize(this.state.model.componentsMap); } _setModel = (model: CustomFieldKindValueBackendViewModel) => { this.setState({ model: model }); } _handleFieldTypeChange = (newFieldType: string) => { var clone = _.clone(this.state.model); clone.fieldType = newFieldType; clone.typedViewModel = { type: newFieldType, $type: this._componentsMap[newFieldType].viewModelType }; this._setModel(clone); } _getColumnPrefixOrEmptyString = (entityType: string) => { var entityTypeDto = _.find(this.props.model.entityTypes, et => et.systemName === entityType); return entityTypeDto && entityTypeDto.prefix || ""; } _hanleEntityTypeChange = (newEntityType: string) => { var clone = _.clone(this.state.model); clone.entityType = newEntityType; var columnPrefix = this._getColumnPrefixOrEmptyString(newEntityType); clone.columnName = `${columnPrefix}${this.state.model.systemName || ""}`; this._setModel(clone); } _handleSystemNameChange = (newSystemName: string) => { var clone = _.clone(this.state.model); clone.systemName = newSystemName; var columnPrefix = this._getColumnPrefixOrEmptyString(this.state.model.entityType); clone.columnName = `${columnPrefix}${newSystemName || ""}`; this._setModel(clone); } _renderComponent = () => { var entityTypeSelectOptions = this.state.model.entityTypes.map(et => { return { Text: et.name, Value: et.systemName } }); var fieldTypeSelectOptions = Object.keys(this._componentsMap). map(key => { return { Text: this._componentsMap[key].name, Value: key }; }); var componentInfo = this._componentsMap[this.state.model.fieldType]; var TypedComponent = componentInfo.component; return ( <div> <div className="row form_horizontal"> <FormGroup label=" " validationMessage={this.state.validationContext.getValidationMessageFor(m => m.entityType)}> <div className="form-control"> <Select value={this.state.model.entityType} options={entityTypeSelectOptions} width="normal" placeholder=" " onChange={this._hanleEntityTypeChange} /> </div> </FormGroup> <DataGroup label=" " value={this.state.model.columnName} /> <FormGroup label="" validationMessage={this.state.validationContext.getValidationMessageFor(m => m.name)}> <Textbox value={this.state.model.name} width="normal" onChange={getPropertySetter( this.state.model, this._setModel, viewModel => viewModel.name)} /> </FormGroup> <FormGroup label=" " validationMessage={this.state.validationContext.getValidationMessageFor(m => m.systemName)}> <Textbox value={this.state.model.systemName} width="normal" onChange={this._handleSystemNameChange} /> </FormGroup> <FormGroup label=" " validationMessage={this.state.validationContext.getValidationMessageFor(m => m.fieldType)}> <div className="form-control"> <Select value={this.state.model.fieldType} options={fieldTypeSelectOptions} width="normal" placeholder=" " onChange={this._handleFieldTypeChange} /> </div> </FormGroup> <TypedComponent validationContext={this.state.validationContext.getValidationContextFor(m => m.typedViewModel)} onChange={getPropertySetter( this.state.model, this._setModel, viewModel => viewModel.typedViewModel)} value={this.state.model.typedViewModel} constantComponentData={componentInfo.constantComponentData} /> <FormGroup> <Checkbox checked={this.state.model.isMultiple} label="       " onChange={getPropertySetter( this.state.model, this._setModel, viewModel => viewModel.isMultiple)} disabled={false} /> </FormGroup> {this._renderShouldBeExportedCheckbox()} </div> </div>); } _getViewModelValue = () => { var clone = _.clone(this.state.model); clone.componentsMap = null; clone.entityTypes = null; return clone; } render() { return ( <div> <fieldset> {this._renderComponent() } </fieldset> <HiddenInputJsonSerializer model={this._getViewModelValue()} name={this.props.modelName} /> </div>); } _renderShouldBeExportedCheckbox = () => { if (this.state.model.entityType !== "HistoricalCustomer") return null; return ( <FormGroup validationMessage={this.state.validationContext.getValidationMessageFor(m => m.shouldBeExported)}> <Checkbox checked={this.state.model.shouldBeExported} label="   " onChange={getPropertySetter( this.state.model, this._setModel, viewModel => viewModel.shouldBeExported)} disabled={false} /> </FormGroup>); } } } 


TypeScript


「それは䜕でしたか」javascriptが衚瀺されるこずを期埅しおいるかどうかを尋ねるこずができたす。 これはtsxです-TypeScriptでのReactのjsxのバリアントです。 UIは完党に静的に型付けされおおり、「マゞックラむン」はありたせん。 同意したす、これは私たちのような筋金入りのバック゚ンドから期埅できたす:)
いく぀かの蚀葉がありたす。 私は静的および動的に型付けされた蚀語のトピックでホリバヌを䞊げるずいう目暙はありたせん。 私たちの䌚瀟では、動的蚀語を奜む人はいたせんでした。 私たちは、長幎にわたっおリファクタリングされおきた倧芏暡なサポヌトプロゞェクトを䜜成するこずはそれほど難しくないず考えおいたす。 IntelliSenseが機胜しないため、曞くのは難しいです:)これが私たちの信念です。 すべおをテストでカバヌできるず䞻匵するこずができ、それから動的に型付けされた蚀語で可胜になりたすが、このトピックに぀いおは議論したせん。
tsx圢匏は、スタゞオず別の非垞に重芁なポむントである新しいRでサポヌトされおいたす。 しかし、1幎前Rずは異なりスタゞオではjsxのサポヌトさえありたせんでした。jsの開発には別のコヌド゚ディタヌが必芁でしたSublimeずAtomを䜿甚したした。 この結果、スタゞオ゜リュヌションではファむルの半分が十分ではなく、肉屋が远加されただけでした。 幞犏はすでに来おいるので、それに぀いおは話したしょう。
玔粋な圢匏のtypescriptでさえ、私たちが望む静的型付けのレベルを䞎えないこずに泚意すべきです。 たずえば、モデルにいく぀かのプロパティを蚭定する堎合実際にUIコントロヌラヌをいく぀かのモデルプロパティにバむンドするため、そのようなプロパティごずに長い間コヌルバック関数を蚘述し、プロパティの名前を取るコヌルバックを1぀䜿甚できたす。静的に入力されるこずはありたせん。 具䜓的には、この問題をおよそこのコヌドで解決したした䞊蚘のgetPropertySetterの䜿甚䟋を参照できたす。
 /// <reference path="../../libraries/underscore.d.ts"/> function getPropertySetter<TViewModel, TProperty>( viewModel: TViewModel, viewModelSetter: {(viewModel: TViewModel): void}, propertyExpression: {(viewModel: TViewModel): TProperty}): {(newPropertyValue: TProperty): void} { return (newPropertyValue: TProperty) => { var viewModelClone = _.clone(viewModel); var propertyName = getPropertyNameByPropertyProvider(propertyExpression); viewModelClone[propertyName] = newPropertyValue; viewModelSetter(viewModelClone); }; } function getPropertyName<TObject>(obj: TObject, expression: {(obj: TObject): any}): string { return getPropertyNameByPropertyProvider(expression); } function getPropertyNameByPropertyProvider(propertyProvider: Function): string { return /\.([^\.;]+);?\s*\}$/.exec(propertyProvider.toString())[1]; } 

getPropertyNameByPropertyProviderの実装が非垞に銬鹿げおいるこずは間違いありたせん別の単語を遞ぶこずすらありたせん。 しかし、typescriptはただ別の遞択肢を提䟛したせん。 ExpressionTreeずnameofは含たれおいたせん。getPropertySetterのプラスの偎面は、このような実装のマむナスの偎面を䞊回りたす。 最埌に、圌女に䜕が起こる可胜性がありたすか ある時点で速床が䜎䞋し始める可胜性があり、そこにキャッシュを割り圓おるこずができたす。たたは、その頃にはtypescriptのnameofが実行されたす。
このようなハックのおかげで、たずえば、コヌド党䜓で名前を倉曎しおいるので、どこかで䜕かがバラバラになるこずを心配する必芁はありたせん。
そうでなければ、すべおが魔法のように機胜したす。 コンポヌネントに必芁な小道具を指定したせんでしたか コンパむル゚ラヌ。 間違ったタむプのプロップをコンポヌネントに枡したしたか コンパむル゚ラヌ。 実行時の譊告付きの愚かなPropTypeはありたせん。 ここでの唯䞀の問題は、typescriptではなくCでバック゚ンドを保持しおいるこずです。そのため、クラむアントで䜿甚される各モデルは、サヌバヌずクラむアントで2回蚘述する必芁がありたす。 ただし、この問題には解決策がありたす。私たちは、.NETの型からのtypescriptのプロトタむプ型ゞェネレヌタヌを䜜成したした。 このナヌティリティを䜕らかの方法で適甚し、戊闘状態での動䜜を確認する必芁があるようです。 どうやら、すべおがすでに倧䞈倫です。

コンポヌネントレンダリング


ペヌゞを開くずきにコンポヌネントを初期化する方法ず、サヌバヌコヌドず察話する方法に぀いお詳しく説明したす。 カプリングが非垞に高いこずをすぐに譊告したすが、䜕ができたすか。
サヌバヌ䞊の各コンポヌネントには、POST芁求䞭にこのコンポヌネントがバむンドするビュヌモデルがありたす。 通垞、最初からコンポヌネントを初期化するために同じビュヌモデルが䜿甚されたす。 たずえば、䞊蚘の远加フィヌルドペヌゞのビュヌモデルを初期化するコヌドCは次のずおりです。
サヌバヌ䞊のモデル初期化コヌドを衚瀺する
 public void PrepareForViewing(MvcModelContext mvcModelContext) { ComponentsMap = ModelApplicationHostController .Instance .Get<ReactComponentViewModelConfiguration>() .GetNamedObjectRelatedComponentsMapFor<CustomFieldKindTypedViewModelBase, CustomFieldType>( customFieldViewModel => customFieldViewModel.PrepareForViewing(mvcModelContext)); EntityTypes = ModelApplicationHostController.NamedObjects .GetAll<CustomFieldKindEntityType>() .Select( type => new EntityTypeDto { Name = type.Name, SystemName = type.SystemName, Prefix = type.ColumnPrefix }) .ToArray(); if (ModelApplicationHostController.NamedObjects.Get<DirectCrmFeatureComponent>().Sku.IsEnabled()) { EntityTypes = EntityTypes.Where( et => et.SystemName != ModelApplicationHostController.NamedObjects .Get<CustomFieldKindEntityTypeComponent>().Purchase.SystemName) .ToArray(); } else { EntityTypes = EntityTypes.Where( et => et.SystemName != ModelApplicationHostController.NamedObjects .Get<CustomFieldKindEntityTypeComponent>().Sku.SystemName) .ToArray(); } if (FieldType.IsNullOrEmpty()) { TypedViewModel = new StringCustomFieldKindTypedViewModel(); FieldType = TypedViewModel.Type; } } 


ここでは、いく぀かのプロパティずコレクションが初期化され、リストを䜜成するために䜿甚されたす。
このビュヌモデルのデヌタを䜿甚しおコンポヌネントを描画するために、Extensionメ゜ッドHtmlHelperが蚘述されおいたす。 実際、コンポヌネントをレンダリングする必芁がある堎所では、次のコヌドを䜿甚したす。
 @Html.ReactJsFor("DirectCrm.SaveCustomFieldKindComponent", m => m.Value) 

最初のパラメヌタヌはコンポヌネントの名前、2番目はPropertyExpression-このコンポヌネントのデヌタが配眮されおいるペヌゞのビュヌモデルのパスです。 このメ゜ッドのコヌドは次のずおりです。
 public static IHtmlString ReactJsFor<TModel, TProperty>( this HtmlHelper<TModel> htmlHelper, string componentName, Expression<Func<TModel, TProperty>> expression, object initializeObject = null) { var validationData = htmlHelper.JsonValidationMessagesFor(expression); var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData); var modelData = JsonConvert.SerializeObject( metadata.Model, new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto, TypeNameAssemblyFormat = FormatterAssemblyStyle.Full, Converters = { new StringEnumConverter() } }); var initializeData = JsonConvert.SerializeObject(initializeObject); return new HtmlString(string.Format( "<div data-react-component='{0}' data-react-model-name='{1}' data-react-model='{2}' " + "data-react-validation-summary='{3}' data-react-initialize='{4}'></div>", HttpUtility.HtmlEncode(componentName), HttpUtility.HtmlEncode(htmlHelper.NameFor(expression)), HttpUtility.HtmlEncode(modelData), HttpUtility.HtmlEncode(validationData), HttpUtility.HtmlEncode(initializeData))); } 

, div, , : , , , , , - . div :
 function initializeReact(context) { $('div[data-react-component]', context).each(function () { var that = this; var data = $(that).data(); var component = eval(data.reactComponent); if (data.reactInitialize == null) { data.reactInitialize = {}; } var props = $.extend({ model: data.reactModel, validationSummary: data.reactValidationSummary, modelName: data.reactModelName }, data.reactInitialize); React.render( React.createElement(component, props), that ); }); } 

, — state. , ( / select').

Binding


, , ?
. . . , , ( , ), hidden input, , json. , json ASP.NET , ModelBinder.
hidden input'. :
 <HiddenInputJsonSerializer model={this._getViewModelValue() } name={this.props.modelName} /> 

:
 class HiddenInputJsonSerializer extends React.Component<{ model: any, name: string }, {}> { render() { var json = JSON.stringify(this.props.model); var name = this.props.name; return ( <input type="hidden" value={json} name={name} /> ); } } 

— json , this.props.modelName — , data-react-model-name (. ), - -, json'.
, json - , . , -, json', JsonBindedAttribute. -, -, json:
 public class CustomFieldKindCreatePageViewModel : AdministrationSiteMasterViewModel { public CustomFieldKindCreatePageViewModel() { Value = new CustomFieldKindValueViewModel(); } [JsonBinded] public CustomFieldKindValueViewModel Value { get; set; } ///      - } 

, - CustomFieldKindCreatePageViewModel.Value . - — ModelBinder. : JsonBindedAttribute — , CustomFieldKindValueViewModel ( ). :
, json
 public class MindboxDefaultModelBinder : DefaultModelBinder { private object DeserializeJson( string json, Type type, string fieldNamePrefix, ModelBindingContext bindingContext, ControllerContext controllerContext) { var settings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto, MetadataPropertyHandling = MetadataPropertyHandling.ReadAhead, Converters = new JsonConverter[] { new ReactComponentPolimorphicViewModelConverter(), new FormBindedConverter(controllerContext, bindingContext, fieldNamePrefix) } }; return JsonConvert.DeserializeObject(json, type, settings); } protected override void BindProperty( ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor) { if (!propertyDescriptor.Attributes.OfType<JsonBindedAttribute>().Any()) { base.BindProperty(controllerContext, bindingContext, propertyDescriptor); } } public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { var result = base.BindModel(controllerContext, bindingContext); // ... // ,      // ... if (result != null) { FillJsonBindedProperties(controllerContext, bindingContext, result); } return result; } private static string BuildFormVariableFullName(string modelName, string formVariableName) { return modelName.IsNullOrEmpty() ? formVariableName : string.Format("{0}.{1}", modelName, formVariableName); } private void FillJsonBindedProperties( ControllerContext controllerContext, ModelBindingContext bindingContext, object result) { var jsonBindedProperties = result.GetType().GetProperties() .Where(pi => pi.HasCustomAttribute<JsonBindedAttribute>()) .ToArray(); foreach (var propertyInfo in jsonBindedProperties) { var formFieldFullName = BuildFormVariableFullName( bindingContext.FallbackToEmptyPrefix ? string.Empty : bindingContext.ModelName, propertyInfo.Name); if (controllerContext.HttpContext.Request.Params.AllKeys.Contains(formFieldFullName)) { var json = controllerContext.HttpContext.Request.Params[formFieldFullName]; if (!json.IsNullOrEmpty()) { var convertedObject = DeserializeJson( json, propertyInfo.PropertyType, formFieldFullName, bindingContext, controllerContext); propertyInfo.SetValue(result, convertedObject); } } else { throw new InvalidOperationException( string.Format( "    property {0}   {1}.  99.9%      js.", formFieldFullName, result.GetType().AssemblyQualifiedName)); } } } } 


, , json, json , , 99.9% - , - . , .
, , html, , react- . , - react', , react'. , , . , :


, «», js , react — , . js, , html , js . , , UI-, , react. « », , react' .
? , , input' name, , react. input' hidden input', - . , POST-, , -, , JsonBindedAttribute, , json. , - , FormBindedAttribute, json FormBindedConverter, :
FormBindedConverter
 public class FormBindedConverter : JsonConverter { private readonly ControllerContext controllerContext; private readonly ModelBindingContext parentBindingContext; private readonly string formNamePrefix; private Type currentType = null; private static readonly Type[] primitiveTypes = new[] { typeof(int), typeof(bool), typeof(long), typeof(decimal), typeof(string) }; public FormBindedConverter( ControllerContext controllerContext, ModelBindingContext parentBindingContext, string formNamePrefix) { this.controllerContext = controllerContext; this.parentBindingContext = parentBindingContext; this.formNamePrefix = formNamePrefix; } public override bool CanConvert(Type objectType) { return currentType != objectType && !primitiveTypes.Contains(objectType); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { var currentJsonPath = reader.Path; currentType = objectType; var result = serializer.Deserialize(reader, objectType); currentType = null; if (result == null) return null; var resultType = result.GetType(); var formBindedProperties = resultType.GetProperties().Where(p => p.HasCustomAttribute<FormBindedAttribute>()); foreach (var formBindedProperty in formBindedProperties) { var formBindedPropertyName = formBindedProperty.Name; var formBindedPropertyFullPath = $"{formNamePrefix}.{currentJsonPath}.{formBindedPropertyName}"; var formBindedPropertyModelBinderAttribute = formBindedProperty.PropertyType.TryGetSingleAttribute<ModelBinderAttribute>(); var effectiveBinder = GetBinder(formBindedPropertyModelBinderAttribute); var formBindedObject = effectiveBinder.BindModel( controllerContext, new ModelBindingContext(parentBindingContext) { ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType( () => formBindedProperty.GetValue(result), formBindedProperty.PropertyType), ModelName = formBindedPropertyFullPath }); formBindedProperty.SetValue(result, formBindedObject); } return result; } private static IModelBinder GetBinder(ModelBinderAttribute formBindedPropertyModelBinderAttribute) { IModelBinder effectiveBinder; if (formBindedPropertyModelBinderAttribute == null) { effectiveBinder = new MindboxDefaultModelBinder(); } else { effectiveBinder = formBindedPropertyModelBinderAttribute.GetBinder(); } return effectiveBinder; } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { serializer.Serialize(writer, value); } } 


- json, FormBindedAttribute. - , , binder , binder .
MindboxDefaultModelBinder, FormBindedConverter, FilterViewModelBinder, MindboxDefaultModelBinder.

-


UI , . :






UI. , switch , . , , . :
 module DirectCrm { export class StringCustomFieldKindComponent extends CustomFieldKindComponentBase { render() { var stringViewModel = this.props.value as StringCustomerFieldKindTypedBackendViewModel; var stringConstantData = this.props.constantComponentData as StringCustomFieldKindConstantComponentData; var validationContext = this.props.validationContext as IValidationContext<StringCustomerFieldKindTypedBackendViewModel>; return ( <div> {super.render() } <FormGroup label="  " validationMessage={validationContext.getValidationMessageFor(m => m.validationStrategySystemName) } > <div className="form-control"> <Commons.Select value={stringViewModel.validationStrategySystemName} width="normal" onChange={getPropertySetter( stringViewModel, vm => this.props.onChange(vm), m => m.validationStrategySystemName) } options={stringConstantData.validationStrategies} disabled={this.props.disabled}/> </div> </FormGroup> </div>); } } } 

 module DirectCrm { export class DefaultCustomFieldKindComponent extends CustomFieldKindComponentBase { } } 

 module DirectCrm { export class CustomFieldKindComponentBase extends React.Component<DirectCrm.CustomFieldKindTypedComponentProps, {}> { render() { return <FormGroup label = " " validationMessage = { this.props.validationMessageForFieldType } > <div className="form-control"> <Commons.Select value={this.props.fieldType} options={this.props.fieldTypeSelectOptions} width="normal" placeholder=" " onChange={this.props.handleFieldTypeChange} disabled = {this.props.disabled}/> </div> {this.renderTooltip() } </FormGroup> } renderTooltip() { return <Commons.Tooltip additionalClasses="tooltip-icon_help" message={this.props.constantComponentData.tooltipMessage }/> } } } 

?
, :
 _renderComponent = () => { var fieldTypeSelectOptions = Object.keys(this._componentsMap). map(key => { return { Text: this._componentsMap[key].name, Value: key }; }); var componentInfo = this._componentsMap[this.state.model.fieldType]; var TypedComponent = componentInfo.component; return ( <div> <div className="row form_horizontal"> <div className="col-group"> //    <TypedComponent validationContext={this.state.validationContext.getValidationContextFor(m => m.typedViewModel) } onChange={getPropertySetter( this.state.model, this._setModel, viewModel => viewModel.typedViewModel) } value={this.state.model.typedViewModel} fieldType={this.state.model.fieldType} validationMessageForFieldType={this.state.validationContext.getValidationMessageFor(m=> m.fieldType) } fieldTypeSelectOptions={fieldTypeSelectOptions} handleFieldTypeChange={this._handleFieldTypeChange} constantComponentData={componentInfo.constantComponentData} disabled={!this.state.model.isNew}/> </div> //    </div>); } 

, TypedComponent, _componentsMap. _componentsMap — , ( « ») componentInfo, , : , (, url- - ), .NET , , -. _componentsMap json :
ComponentsMap'
 "componentsMap":{ "Integer":{ "name":"", "viewModelType":"Itc.DirectCrm.Web.IntegerCustomFieldKindTypedViewModel, itc.DirectCrm.Web, Version=6.459.0.3741, Culture=neutral, PublicKeyToken=null", "componentName":"DirectCrm.DefaultCustomFieldKindComponent", "constantComponentData":{ "$type":"Itc.DirectCrm.Web.IntegerCustomFieldKindTypedViewModel, itc.DirectCrm.Web, Version=6.459.0.3741, Culture=neutral, PublicKeyToken=null", "tooltipMessage":": 123456", "type":"Integer" } }, "String":{ "name":"", "viewModelType":"Itc.DirectCrm.Web.StringCustomFieldKindTypedViewModel, itc.DirectCrm.Web, Version=6.459.0.3741, Culture=neutral, PublicKeyToken=null", "componentName":"DirectCrm.StringCustomFieldKindComponent", "constantComponentData":{ "$type":"Itc.DirectCrm.Web.StringCustomFieldKindTypedViewModel, itc.DirectCrm.Web, Version=6.459.0.3741, Culture=neutral, PublicKeyToken=null", "validationStrategies":[ { "Disabled":false, "Group":null, "Selected":true, "Text":" ", "Value":"Default" }, { "Disabled":false, "Group":null, "Selected":false, "Text":"    ", "Value":"IsValidLatinStringWithWhitespaces" }, { "Disabled":false, "Group":null, "Selected":false, "Text":"    ", "Value":"IsValidLatinStringWithDigits" }, { "Disabled":false, "Group":null, "Selected":false, "Text":"", "Value":"IsValidDigitString" } ], "validationStrategySystemName":"Default", "tooltipMessage":": \"\"", "type":"String" } }, "Enum":{ "name":"", "viewModelType":"Itc.DirectCrm.Web.EnumCustomFieldKindTypedViewModel, itc.DirectCrm.Web, Version=6.459.0.3741, Culture=neutral, PublicKeyToken=null", "componentName":"DirectCrm.EnumCustomFieldKindComponent", "constantComponentData":{ "$type":"Itc.DirectCrm.Web.EnumCustomFieldKindTypedViewModel, itc.DirectCrm.Web, Version=6.459.0.3741, Culture=neutral, PublicKeyToken=null", "selectedEnumValues":null, "forceCreateEnumValue":false, "tooltipMessage":":   - \"ExternalId\",  - \"123\"", "type":"Enum" } } } 


? . , ComponentsMap - :
 public void PrepareForViewing(MvcModelContext mvcModelContext) { ComponentsMap = ModelApplicationHostController .Instance .Get<ReactComponentViewModelConfiguration>() .GetNamedObjectRelatedComponentsMapFor<CustomFieldKindTypedViewModelBase, CustomFieldType>( customFieldViewModel => customFieldViewModel.PrepareForViewing(mvcModelContext)); //  -  } 

, ReactComponentViewModelConfiguration , - CustomFieldKindTypedViewModelBase, . :
 configuration.RegisterNamedObjectRelatedViewModel<CustomFieldKindTypedViewModelBase, CustomFieldType>( () => new StringCustomFieldKindTypedViewModel()); configuration.RegisterNamedObjectRelatedViewModel<CustomFieldKindTypedViewModelBase, CustomFieldType>( () => new IntegerCustomFieldKindTypedViewModel()); configuration.RegisterNamedObjectRelatedViewModel<CustomFieldKindTypedViewModelBase, CustomFieldType>( () => new EnumCustomFieldKindTypedViewModel()); 

- , . - C# . , .

怜蚌


:

, , - , . - . , . , , , . , , .
. data-react-validation-summary (. ReactJsFor ). Validation summary — json, - ( ), , -. , validationSummary :


, , .
validation summary :
 { "typedViewModel":{ "selectedEnumValues[0]":{ "systemName":[ "      250 " ] } }, "name":[ " " ] } 

, — , , . ValidationContext, validation summary, :
 interface IValidationContext<TViewModel> { isValid: boolean; getValidationMessageFor: { (propertyExpression: {(model: TViewModel):any}): JSX.Element }; validationMessageExpandedFor: { (propertyExpression: {(model: TViewModel):any}): JSX.Element }; getValidationContextFor: { <TProperty>(propertyExpression: {(model: TViewModel):TProperty}): IValidationContext<TProperty> }; getValidationContextForCollection: { <TProperty>(propertyExpression: {(model: TViewModel):TProperty[]}): {(index: number): IValidationContext<TProperty>} } } 

, . . , «»:
 <FormGroup label="" validationMessage={this.state.validationContext.getValidationMessageFor(m => m.name) }> <Commons.Textbox value={this.state.model.name} width="normal" onChange={getPropertySetter( this.state.model, this._setModel, viewModel => viewModel.name) } /> </FormGroup> 

this.state.validationContext IValidationContext<CustomFieldKindValueBackendViewModel>, . getPropertyNameByPropertyProvider, , getValidationMessageFor validation summary .
, validation summary .
, - , . , , . , — -. - - . , , . , — , , , , , validation summary.
- «» :
 private void RegisterEndUserInput( ISubmodelInputRegistrator<CustomFieldKindValueViewModel> registrator, CustomFieldKind customFieldKind) { //   registrator.RegisterEndUserInput( customFieldKind, cfk => cfk.Name, this, m => m.Name); //   } 

this — -, Name, , Name CustomFieldKind customFieldKind. , Name -.
CustomFieldKind :
 public void Validate(ValidationContext validationContext) { //    validationContext .Validate(this, cfk => cfk.Name) .ToHave(() => !Name.IsNullOrEmpty()) .OrAddError<CustomFieldCustomizationTemplateComponent>(c => c.NameRequired); //    } 

, , , CustomFieldKind.Name , , .

結論ずしお


, UI. , , , :)
, , - , UI , Enterprise. , . ReactJS, - . -, , , , ! .

Source: https://habr.com/ru/post/J276235/


All Articles