点検報告機能導入計画の具体的な分析で安心の運用・保守を実現

ビッグデータ技術の進化と情報セキュリティへの需要の高まりに伴い、データ規模の継続的な拡大により、データの運用と保守作業に深刻な課題が生じています。膨大なデータによる大きな管理プレッシャーに直面し、運用保守担当者は効率のボトルネックに直面しており、人件費の上昇により、問題解決のために運用保守チームの拡大のみに頼ることはもはや現実的ではありません。

インテリジェンス、効率、利便性が運営と保守の発展にとって避けられない方向であることがわかります。カンガルークラウドが開始した検査レポート機能は、まさにこの目標に応え、最適なソリューションを提供することに取り組んでいます。

検査報告書とは何ですか?

検査報告書とは、特定のシステムまたは機器の包括的な検査を実施し、検査結果と提案を報告書にまとめるプロセスを指します。検査レポートは通常、システムや機器の動作状態とパフォーマンスを評価するために使用され、問題の特定、システムの最適化、効率の向上、故障率の削減の参考となります。

ファイル

この記事では、検査レポートのさまざまな機能の特徴とその実装計画について詳しく説明し、そのようなニーズを持つユーザーに実用的な参考情報を提供します。

検査報告書実施機能

● カスタムレイアウト

·レポート内のパネルをドラッグ アンド ドロップしてレイアウトを変更できます。

· ドラッグ処理中のドラッグ領域を制限します。第 1 レベルのディレクトリを別の第 1 レベルのディレクトリに移動するなど、ディレクトリを越えた移動は許可されません。セカンダリディレクトリになる

● ディレクトリは縮小および拡張可能

· ディレクトリは縮小と展開をサポートしています。縮小するとすべてのサブパネルが非表示になり、展開するとすべてのサブパネルが表示されます。

· ディレクトリを移動すると、サブパネルも移動に追従します

・ディレクトリを変更すると、右側のディレクトリパネルも同時に更新されます。

·カタログ番号の生成

ファイル

● 右側のディレクトリツリー

· カタログ番号の生成

·アンカースクロールをサポート

・拡張と縮小をサポート

・左のレポートとリンクしています

ファイル

● データパネル

・日付範囲に基づいてインジケーターデータを取得します

・指標情報をチャート形式で表示

・詳細の表示、削除

· 更新リクエストをサポートするための各パネルのリクエスト設計

ファイル

ファイル

●パネルインポート

・カタログで選択したパネルの数をカウントします。

· 新しいパネルをインポートする場合、既存のレイアウトは破棄できず、新しいパネルは古いパネルに従うことしかできません。

・既存のパネルをインポートする場合、データの比較を行う必要があり、データに変更があった場合には、最新のデータを再度取得する必要があります。

ファイル

● 保存

保存する前は、パネルのインポートを含め、レイアウトに関連するすべての操作は一時的なものです。 「保存」をクリックした後でのみ、現在のデータが保存のためにバックエンドに送信されます。

● PDF および Word のエクスポートをサポート

ファイル

検査報告実施計画

では、この一連の検査レポート機能はどのように実装されているのでしょうか?以下では、データ構造設計コンポーネント設計、ディレクトリ、パネルなどの各側面を紹介します。

データ構造設計

まずフラット構造を使用した図を見てみましょう。

ファイル

フラット構造では、次の行パネルを検索するだけで子を決定できます。これは複数レベルのディレクトリにも当てはまりますが、最初のレベルのディレクトリには追加の処理が必要です。

フラット構造は実装が比較的簡単ですが、特定のニーズを満たすため、つまりディレクトリのドラッグ アンド ドロップを制限するためです。ディレクトリを制限するには、比較的明確なパネルの階層関係が必要であることは明らかです。ツリー データ構造は、データの階層構造を非常に適切かつ明確に記述することができます。

ファイル

コンポーネント設計

従来のコンポーネント プログラミングとは異なります。実装に関しては、レンダリングとデータ処理は分離され、次の 2 つの部分に分かれます。

· React コンポーネント: 主にページのレンダリングを担当します。

· クラス: データ処理を担当します。

ファイル

ダッシュボードモデル

class DashboardModel {
    id: string | number;
    panels: PanelModel[]; // 各个面板
    // ...
}

パネルモデル

class PanelModel {
    key?: string;
    id!: number;
    gridPos!: GridPos; // 位置信息
    title?: string;
    type: string;
    panels: PanelModel[]; // 目录面板需要维护当前目录下的面板信息
    // ...
}

各 Dashboard コンポーネントはDashboardModelに対応し、各Panel コンポーネントはPanelModelに対応します。

React コンポーネントは、クラス インスタンス内のデータに基づいてレンダリングされます。インスタンスが生成された後は、簡単に破棄されたり、参照アドレスが変更されたりすることはありません。これにより、レンダリングにインスタンス データに依存するReact コンポーネントが更新レンダリングをトリガーすることがなくなります。

インスタンス内のデータが変更された後に、コンポーネントの更新レンダリングを手動でトリガーする方法が必要です。

● コンポーネントレンダリング制御

以前に Hooks コンポーネントを使用したため、Class コンポーネントとは異なり、forceUpdate メソッドを呼び出すことでコンポーネントをトリガーできます。

React18 にはuseSyncExternalStoreという新機能があり、データが変更された場合に外部データをサブスクライブできるようになり、コンポーネントのレンダリングがトリガーされます。

実際、useSyncExternalStore がコンポーネントのレンダリングをトリガーする原理は、状態の値が変更されると、外部コンポーネントのレンダリングが発生することにより内部で状態を維持することです。

この考えに基づいて、コンポーネントのレンダリングをトリガーできるuseForceUpdate メソッドを実装しました。

export function useForceUpdate() {
    const [_, setValue] = useState(0);
    return debounce(() => setValue((prevState) => prevState + 1), 0);
}

useForceUpdateは実装されていますが、実際に使用する場合にはコンポーネントが破棄される際にイベントを削除する必要があります。 UseSyncExternalStore は内部的に実装されており、直接使用できます。

useSyncExternalStore(dashboard?.subscribe ?? (() => {}), dashboard?.getSnapshot ?? (() => 0));

useSyncExternalStore(panel?.subscribe ?? (() => {}), panel?.getSnapshot ?? (() => 0));

useSyncExternalStore の使用状況に応じて、subscribe メソッドとgetSnapshot メソッドがそれぞれ追加されます

class DashboardModel {  // PanelModel 一样 
    count = 0;

    forceUpdate() {
        this.count += 1;
        eventEmitter.emit(this.key);
    }

    /**
     * useSyncExternalStore 的第一个入参,执行 listener 可以触发组件的重渲染
     * @param listener
     * @returns
     */
    subscribe = (listener: () => void) => {
        eventEmitter.on(this.key, listener);
        return () => {
            eventEmitter.off(this.key, listener);
        };
    };

    /**
     * useSyncExternalStore 的第二个入参,count 在这里改变后触发diff的通过。
     * @param listener
     * @returns
     */
    getSnapshot = () => {
        return this.count;
    };
}

データが変更され、コンポーネントのレンダリングをトリガーする必要がある場合は、forceUpdate を実行するだけです。

パネル

●パネルドラッグ

市場で最も人気のあるドラッグ アンド ドロップ プラグインには次のようなものがあります。

· 反応-美しい-dnd

· 反応-dnd

· 反応グリッドレイアウト

比較した結果、 react-grid-layout はパネルのドラッグ アンド ドロップ機能に非常に適していることがわかりました。 act-grid-layout 自体は使い方が簡単で、基本的に敷居がありません。最終的には、react-grid-layout を使用することにしました。詳細な手順については、次のリンクを参照してください: https://github.com/react-grid-layout/react-grid-layout

パネル レイアウトが変更されると、 react-grid-layout のonLayoutChange メソッドがトリガーされ、レイアウト後のすべてのパネルの最新の位置データが取得されます。

const onLayoutChange = (newLayout: ReactGridLayout.Layout[]) => {
    for (const newPos of newLayout) {
        panelMap[newPos.i!].updateGridPos(newPos);
    }
    dashboard!.sortPanelsByGridPos();
};

PanelMap はマップ、キーはPanel.key、値はパネルです。これはコンポーネントがレンダリングされるときに準備が整います。

const panelMap: Record<PanelModel['key'], PanelModel> = {};

パネル レイアウト データを更新するには、PanelMap を通じて対応するパネルを正確に特定し、さらにそのupdateGridPos メソッドを呼び出してレイアウト更新操作を実行します。

この時点では、パネル自体のデータ更新のみが完了しています。また、ダッシュボードの sortPanelsByGridPos メソッドを実行して、すべてのパネルを並べ替える必要があります。

class DashboardModel {
    sortPanelsByGridPos() {
        this.panels.sort((panelA, panelB) => {
            if (panelA.gridPos.y === panelB.gridPos.y) {
                return panelA.gridPos.x - panelB.gridPos.x;
            } else {
                return panelA.gridPos.y - panelB.gridPos.y;
            }
        });
    }
    // ...
}

●パネルドラッグ範囲

現在のドラッグ範囲はダッシュボード全体であり、自由にドラッグできます。緑色はダッシュボードのドラッグ可能な領域、灰色はパネルです。次のように:

ファイル

制限が必要な場合は、以下のような構造に変更する必要があります。

ファイル

オリジナルに基づいて、緑色が全体の移動可能な領域、黄色が第 1 レベルのディレクトリ ブロックに分割されており、ドラッグする場合は、黄色のブロック全体がドラッグに使用されます。紫は第 2 レベルのディレクトリです。ブロックは現在の黄色の領域内でドラッグできます。灰色のパネルは現在のディレクトリ内でのみドラッグできます。

元のデータ構造に基づいて変換する必要があります。

ファイル

class PanelModel {
    dashboard?: DashboardModel; // 当前目录下的 dashboard
    // ...
}

● パネルのデザインをインポート

ファイル

バックエンドから返されるデータは 3 つのレベルを持つツリーであり、それを取得した後、そのデータを ModuleMap、DashboardMap、PanelMap の 3 つのマップに保持します。

import { createContext } from 'react';

export interface Module { // 一级目录
    key: string;
    label: string;
    dashboards?: string[];
    sub_module?: Dashboard[];
}

export interface Dashboard { // 二级目录
    key: string;
    dashboard_key: string;
    label: string;
    panels?: number[];
    selectPanels?: number[];
    metrics?: Panel[];
}

export interface Panel {
    expr: Expr[]; // 数据源语句信息
    label: string;
    panel_id: number;
}

type Expr = {
    expr: string;
    legendFormat: string;
};

export const DashboardContext = createContext({
    moduleMap: new Map<string, Module>(),
    dashboardMap: new Map<string, Dashboard>(),
    panelMap: new Map<number, Panel>(),
});

モジュールをレンダリングするときは、ModuleMap を走査し、モジュール内のダッシュボード情報を通じて 2 番目のディレクトリを見つけます。

インタラクションで第 1 レベルのディレクトリを選択できないように設定します。第 2 レベルのディレクトリが選択されると、関連するパネルが第 2 ディレクトリのダッシュボードのパネルから検索され、適切な領域に表示されます。

これら 3 つのマップの操作については、useHandleData で維持され、エクスポートされます。

{
    ...map, // moduleMap、dashboardMap、panelMap
    getData, // 生成巡检报告的数据结构
    init: initData, // 初始化 Map
}

●パネル選択埋め戻し材

パネル管理に入るとき、選択したパネルをバックフィルする必要があります。getSaveModel を通じて現在の検査レポートの情報を取得し、対応する選択された情報を selectPanels に保存します

ここで必要なのは、selectPanels の値を変更して、対応するパネルを選択することだけです。

●パネル選択リセット

DashboardMap を直接走査し、各 selectPanel をリセットします。

dashboardMap.forEach((dashboard) => {
    dashboard.selectPanels = [];
});

●パネル挿入

パネルを選択した後、選択したパネルを挿入するときにいくつかの状況が発生します。

・今回は元々検査レポートに存在していたパネルも選択されます。データが変更された場合は、最新のデータソース情報に基づいてデータを要求し、レンダリングする必要があります。

・検査レポートにもともと存在していたパネルは今回は選択されていませんので、挿入する際は選択されていないパネルを削除する必要があります。

· 新しく選択したパネルは、挿入時に対応するディレクトリの最後に挿入されます。

新しいパネルを追加するには、ディレクトリの縮小と同様の必要がありますが、次の点が異なります。

· ディレクトリの縮小は 1 つのディレクトリのみを対象としますが、挿入はディレクトリ全体を対象とします。

· ディレクトリの縮小は子ノードから直接バブルアップされますが、挿入はルート ノードから開始され、挿入が完了すると、最新のディレクトリ データに基づいてレイアウトが更新されます。

class DashboardModel {
    update(panels: PanelData[]) {
        this.updatePanels(panels); // 更新面板
        this.resetDashboardGridPos(); // 重新布局
        this.forceUpdate();
    }

    /**
     * 以当前与传入的进行对比,以传入的数据为准,并在当前的顺序上进行修改
     * @param panels
     */
    updatePanels(panels: PanelData[]) {
        const panelMap = new Map();
        panels.forEach((panel) => panelMap.set(panel.id, panel));

        this.panels = this.panels.filter((panel) => {
            if (panelMap.has(panel.id)) {
                panel.update(panelMap.get(panel.id));
                panelMap.delete(panel.id);
                return true;
            }
            return false;
        });

        panelMap.forEach((panel) => {
            this.addPanel(panel);
        });
    }

    addPanel(panelData: any) {
        this.panels = [...this.panels, new PanelModel({ ...panelData, top: this })];
    }

    resetDashboardGridPos(panels: PanelModel[] = this.panels) {
        let sumH = 0;
        panels?.forEach((panel: any | PanelModel) => {
            let h = ROW_HEIGHT;
            if (isRowPanel(panel)) {
                h += this.resetDashboardGridPos(panel.dashboard.panels);
            } else {
                h = panel.getHeight();
            }

            const gridPos = {
                ...panel.gridPos,
                y: sumH,
                h,
            };
            panel.updateGridPos({ ...gridPos });
            sumH += h;
        });

        return sumH;
    }
}

class PanelModel {
    /**
     * 更新
     * @param panel
     */
    update(panel: PanelData) {
        // 数据源语句发生变化需要重新获取数据
        if (this.target !== panel.target) {
            this.needRequest = true;
        }

        this.restoreModel(panel);

        if (this.dashboard) {
            this.dashboard.updatePanels(panel.panels ?? []);
        }

        this.needRequest && this.forceUpdate();
    }
}

●パネルリクエスト

needRequest は、パネルがリクエストを行う必要があるかどうかを制御します。これが true の場合、次回パネルがレンダリングされるときにリクエストが行われ、リクエストの処理もPanelModelに配置されます。

import { Params, params as fetchParams } from '../../components/useParams';

class PanelModel {
    target: string; // 数据源信息

    getParams() {
        return {
            targets: this.target,
            ...fetchParams,
        } as Params;
    }

    request = () => {
        if (!this.needRequest) return;
        this.fetchData(this.getParams());
    };

    fetchData = async (params: Params) => {
        const data = await this.fetch(params);
        this.data = data;
        this.needRequest = false;
        this.forceUpdate();
    };
    
    fetch = async (params: Params) => { /* ... */ }
}

通常、データ レンダリング コンポーネントには深いレベルがあり、リクエストを行う際には時間間隔などの外部パラメータが必要です。これらのパラメータはグローバル変数と useParamsの形式で維持されます。上位コンポーネントは変更を使用してパラメータを変更し、データ レンダリング コンポーネントはスローされたパラメータに基づいてリクエストを作成します。

export let params: Params = {
    decimal: 1,
    unit: null,
};

function useParams() {
    const change = (next: (() => Params) | Params) => {
        if (typeof next === 'function') params = next();
        params = { ...params, ...next } as Params;
    };

    return { params, change };
}

export default useParams;

●パネルリフレッシュ

ルート ノードから下方向に検索してリーフ ノードを見つけ、対応するリクエストをトリガーします。

ファイル

class DashboardModel {
    /**
     * 刷新子面板
     */
    reloadPanels() {
        this.panels.forEach((panel) => {
            panel.reload();
        });
    }
}

class PanelModel {
    /**
     * 刷新
     */
    reload() {
        if (isRowPanel(this)) {
            this.dashboard.reloadPanels();
        } else {
            this.reRequest();
        }
    }

    reRequest() {
        this.needRequest = true;
        this.request();
    }
}

●パネルの削除

パネルを削除するには、対応するダッシュボードの下でパネルを削除するだけで済みます。削除後は、現在のダッシュボードの高さが変更されます。このプロセスは、以下のディレクトリの縮小と一致します。

class DashboardModel {
    /**
     * @param panel 删除的面板
     */
    removePanel(panel: PanelModel) {
        this.panels = this.filterPanelsByPanels([panel]);

        // 冒泡父容器,减少的高度
        const h = -panel.gridPos.h;
        this.top?.changeHeight(h);

        this.forceUpdate();
    }

    /**
     * 根据传入的面板进行过滤
     * @param panels 需要过滤的面板数组
     * @returns 过滤后的面板
     */
    filterPanelsByPanels(panels: PanelModel[]) {
        return this.panels.filter((panel) => !panels.includes(panel));
    }
    // ...
}

● パネルを保存する

バックエンドと通信した後、現在の検査レポートのデータ構造はフロントエンドによって独立して維持され、最終的に文字列がバックエンドに渡されます。現在のパネルデータを取得してJSONに変換します。

パネルの情報取得処理は、ルートノードから始まり、葉ノードにたどり、さらに葉ノードから階層ごとに上に戻るというバックトラッキングの処理となります。

class DashboardModel {
    /**
     * 获取所有面板数据
     * @returns
     */
    getSaveModel() {
        const panels: PanelData[] = this.panels.map((panel) => panel.getSaveModel());
        return panels;
    }
    // ...
}

// 最终保存时所需要的属性,其他的都不需要
const persistedProperties: { [str: string]: boolean } = {
    id: true,
    title: true,
    type: true,
    gridPos: true,
    collapsed: true,
    target: true,
};

class PanelModel {
    /**
     * 获取所有面板数据
     * @returns
     */
    getSaveModel() {
        const model: any = {};

        for (const property in this) {
            if (persistedProperties[property] && this.hasOwnProperty(property)) {
                model[property] = cloneDeep(this[property]);
            }
        }
        model.panels = this.dashboard?.getSaveModel() ?? [];

        return model;
    }
    // ...
}

●パネル詳細表示

ファイル

パネルを表示すると、時刻などを変更できます。これらの操作はインスタンス内のデータに影響するため、元のデータと詳細内のデータを区別する必要があります。

元のパネル データからPanelModelインスタンスを再生成することにより、このインスタンスに対する操作は元のデータに影響を与えません。

const model = panel.getSaveModel();
const newPanel = new PanelModel({ ...model, top: panel.top }); // 创建一个新的实例
setEditPanel(newPanel); // 设置为详情

dom では、詳細ページは絶対配置を使用し、検査レポートをカバーします。

目次

● ディレクトリの縮小と拡張

パネルの非表示と表示を制御するには、ディレクトリ パネルの折りたたまれたプロパティを維持します。

class PanelModel {
    collapsed?: boolean; // type = row
    // ...
}

// 组件渲染
{!collapsed && <DashBoard dashboard={panel.dashboard} serialNumber={serialNumber} />}

ディレクトリを縮小および拡張すると、その高さが変更されます。この変更された高さを上位レベルのダッシュボードに同期する必要があります。

上位レベルで行う必要があることは、制御ディレクトリの処理と似ています。次のように、最初のセカンダリ ディレクトリの縮小を制御します

ファイル

パネルに変更が発生した場合、上位パネルに通知し、対応する操作を実行する必要があります。

ファイル

トップを追加して親インスタンスを取得します。

class DashboardModel {
    top?: null | PanelModel; // 最近的 panel 面板

    /**
     * 面板高度变更,同步修改其他面板进行对应高度 Y 轴的变更
     * @param row 变更高度的 row 面板
     * @param h 变更高度
     */
    togglePanelHeight(row: PanelModel, h: number) {
        const rowIndex = this.getIndexById(row.id);

        for (let panelIndex = rowIndex + 1; panelIndex < this.panels.length; panelIndex++) {
            this.panels[panelIndex].gridPos.y += h;
        }
        this.panels = [...this.panels];

        // 顶级 dashBoard 容器没有 top
        this.top?.changeHeight(h);
        this.forceUpdate();
    }
    // ...
}

class PanelModel {
    top: DashboardModel; // 最近的 dashboard 面板

    /**
     * @returns h 展开收起影响的高度
     */
    toggleRow() {
        this.collapsed = !this.collapsed;
        let h = this.dashboard?.getHeight();
        h = this.collapsed ? -h : h;
        this.changeHeight(h);
    }

    /**
     *
     * @param h 变更的高度
     */
    changeHeight(h: number) {
        this.updateGridPos({ ...this.gridPos, h: this.gridPos.h + h }); // 更改自身面板的高度
        this.top.togglePanelHeight(this, h); // 触发父级变更
        this.forceUpdate();
    }
    // ...
}

最上位のダッシュボードに至るまで、プロセスとバブリングの種類を整理します。伸びるのも縮むのも同じです。

ファイル

● 適切なディレクトリのレンダリング

アンカーポイント/シリアル番号

· アンカーポイントはアンカー + ID を使用してコンポーネントを選択します

・各レンダリングに基づいてシリアル番号が生成されます

パブリッシュとサブスクライブを使用してレンダリングを管理する

ダッシュボードのレイアウトが変更されるたびに、右側のディレクトリを同期して更新する必要があり、任意のパネルで右側のディレクトリの更新をトリガーする必要がある場合があります。

インスタンス内で対応するコンポーネントのレンダリング イベントを維持する場合、次の 2 つの問題が発生します。

・例えばパネルを更新する際に右側のディレクトリのレンダリングをトリガーする必要がないなど区別する必要がある

· 各パネルが右側のディレクトリのレンダリング イベントをサブスクライブする方法

最後に、イベントを管理するためにパブリッシュ/サブスクライバー モデルが採用されました

class EventEmitter {
    list: Record<string, any[]> = {};

    /**
     * 订阅
     * @param event 订阅事件
     * @param fn 订阅事件回调
     * @returns
     */
    on(event: string, fn: () => void) {}

    /**
     * 取消订阅
     * @param event 订阅事件
     * @param fn 订阅事件回调
     * @returns
     */
    off(event: string, fn: () => void) {}

    /**
     * 发布
     * @param event 订阅事件
     * @param arg 额外参数
     * @returns
     */
    emit(event: string, ...arg: any[]) {
}
eventEmitter.emit(this.key); // 触发面板的订阅事件

eventEmitter.emit(GLOBAL); // 触发顶级订阅事件,就包括右侧目录的更新

PDF/Wordエクスポート

PDF エクスポートは html2Canvas + jsPDF で実装されています。画像が長すぎる場合、PDF は画像を分割し、コンテンツ領域が分割される可能性があることに注意してください。パネルの高さを手動で計算して、現在のドキュメントを超えるかどうかを確認する必要があります。高さを超える場合は、事前に分割して次のページに追加する必要があります。ディレクトリ パネルとデータ パネルを一緒に分割してみてください。できるだけ。

Word エクスポートは html-docx-js によって実装されます。ディレクトリの構造を保持し、パネルの下に概要を追加する必要があります。これには、各パネルの画像を個別に変換する必要があります。

実装の考え方は、パネルを走査することです。ディレクトリ パネルを見つけるには、h1 タグと h2 タグを使用してそれを挿入します。データ パネルの場合は、DOM 情報を取得できるようにデータ パネルに ref 属性を保持します。現在のパネルを作成し、これに基づいて、base64 形式で画像変換を実行します (Word では、base64 画像の挿入のみがサポートされます)。

最後に書きます

現在のバージョンの検査レポートはまだ初期段階にあり、最終的な形ではありません。今後のバージョンアップを繰り返しながら、概要説明を含む複数の機能を徐々に追加していきます。

現在の方法で実装した後、将来 UI インターフェイスを調整する必要がある場合は、円グラフや表などの追加など、目的を絞った方法で関連する UI コンポーネントのみを変更する必要があります。データ対話レベルでの変更の場合は、DashboardModel とPanelModel を入力するだけで必要な更新を行うことができます。さらに、特定のシナリオでは、処理用の特別なクラスを柔軟に抽出して、反復プロセス全体がよりモジュール化され効率的になるようにすることもできます。

「Dutstack 製品ホワイトペーパー」ダウンロードアドレス:https://www.dtstack.com/resources/1004 ?src=szsm

「データ ガバナンス業界実践ホワイト ペーパー」ダウンロード アドレス: https://www.dtstack.com/resources/1001?src=szsm

ビッグデータ製品、業界ソリューション、顧客事例について詳しく知りたい、または相談したい場合は、Kangaroo Cloud 公式 Web サイトをご覧ください: https://www.dtstack.com/?src=szkyzg

ライナスは、カーネル開発者がタブをスペースに置き換えるのを防ぐことに自ら取り組みました。 彼の父親はコードを書くことができる数少ないリーダーの 1 人であり、次男はオープンソース テクノロジー部門のディレクターであり、末息子はオープンソース コアです。寄稿者Robin Li: 自然言語 新しいユニバーサル プログラミング言語になるでしょう。オープン ソース モデルは Huawei にますます後れをとっていきます 。一般的に使用されている 5,000 のモバイル アプリケーションを Honmeng に完全に移行するには 1 年かかります。 リッチテキスト エディタ Quill 2.0 リリースされ、機能、信頼性、開発者は「恨みを取り除く ために握手を交わしました。 Laoxiangji のソースはコードではありませんが、その背後にある理由は非常に心温まるものです。Googleは大規模な組織再編を発表しました。
{{名前}}
{{名前}}

おすすめ

転載: my.oschina.net/u/3869098/blog/11046131
おすすめ