You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
-Panel// 面板,wrapper around PanelLayout-BoxPanel// wrapper around a BoxLayout , 将子 widgets 按照行或列的方式排列-SplitPanel// wrapper around a SplitLayout , arranges its widgets into resizable sections.-StackedPanel// wrapper around a StackedLayout , visible widgets are stacked atop one another-CommandPalette// displays command items as a searchable palette-Menu// displays items as a canonical menu-TabBar// displays titles as a single row or column of tabs-DockPanel// 提供灵活的 docking area-MenuBar// canonical menu bar-ScrollBar// canonical scroll bar-TabPanel// combines a TabBar and a StackedPanel
/** * The namespace for the `Widget` class statics. */exportdeclarenamespaceWidget{/** * Construct a new widget. * * @param options - The options for initializing the widget. */constructor(options?: Widget.IOptions);/** * Get the DOM node owned by the widget. */
readonly node: HTMLElement;
readonly title: Title<Widget>;
parent: Widget|null;
layout: Layout|null;children(): IIterator<Widget>;/** * Post an `'update-request'` message to the widget. * * #### Notes * This is a simple convenience method for posting the message. */update(): void;/** * Attach a widget to a host DOM node. * * @param widget - The widget of interest. * * @param host - The DOM node to use as the widget's host. * * @param ref - The child of `host` to use as the reference element. * If this is provided, the widget will be inserted before this * node in the host. The default is `null`, which will cause the * widget to be added as the last child of the host. */functionattach(widget: Widget,host: HTMLElement,ref?: HTMLElement|null): void;}
// packages/core/src/browser/frontend-application.ts
@injectable()exportclassFrontendApplication{/** * Start the frontend application. * * Start up consists of the following steps: * - start frontend contributions * - attach the application shell to the host element * - initialize the application shell layout * - reveal the application shell if it was hidden by a startup indicator */asyncstart(): Promise<void>{awaitthis.startContributions();this.stateService.state='started_contributions';consthost=awaitthis.getHost();this.attachShell(host);awaitanimationFrame();this.stateService.state='attached_shell';awaitthis.initializeLayout();this.stateService.state='initialized_layout';awaitthis.fireOnDidInitializeLayout();awaitthis.revealShell(host);this.registerEventListeners();this.stateService.state='ready';}/** * Attach the application shell to the host element. If a startup indicator is present, the shell is * inserted before that indicator so it is not visible yet. */protectedattachShell(host: HTMLElement): void{constref=this.getStartupIndicator(host);Widget.attach(this.shell,host,ref);// 本质是调用 host.insertBefore(widget.node, ref);}}
/** * General options for the application shell. These are passed on construction and can be modified * through dependency injection (`ApplicationShellOptions` symbol). */exportinterfaceOptionsextendsWidget.IOptions{
bottomPanel: BottomPanelOptions;
leftPanel: SidePanel.Options;
rightPanel: SidePanel.Options;}exportinterfaceBottomPanelOptionsextendsSidePanel.Options{}/** * The default values for application shell options. */exportconstDEFAULT_OPTIONS=Object.freeze(<Options>{bottomPanel: Object.freeze(<BottomPanelOptions>{emptySize: 140,expandThreshold: 160,expandDuration: 0,initialSizeRatio: 0.382}),
leftPanel: Object.freeze(<SidePanel.Options>{emptySize: 140,expandThreshold: 140,expandDuration: 0,initialSizeRatio: 0.191}),
rightPanel: Object.freeze(<SidePanel.Options>{emptySize: 140,expandThreshold: 140,expandDuration: 0,initialSizeRatio: 0.191})
});
在 ApplicationShell 中初始化并拼装。
// packages/core/src/browser/shell/application-shell.ts/** * The application shell manages the top-level widgets of the application. Use this class to * add, remove, or activate a widget. */
@injectable()exportclassApplicationShellextendsWidget{/** * Construct a new application shell. */constructor(
@inject(DockPanelRendererFactory)protecteddockPanelRendererFactory: ()=>DockPanelRenderer,
@inject(StatusBarImpl)protectedreadonlystatusBar: StatusBarImpl,
@inject(SidePanelHandlerFactory)sidePanelHandlerFactory: ()=>SidePanelHandler,
@inject(SplitPositionHandler)protectedsplitPositionHandler: SplitPositionHandler,
@inject(FrontendApplicationStateService)protectedreadonlyapplicationStateService: FrontendApplicationStateService,
@inject(ApplicationShellOptions) @optional()options: RecursivePartial<ApplicationShell.Options>={}){super(optionsasWidget.IOptions);this.addClass(APPLICATION_SHELL_CLASS);this.id='theia-app-shell';// Merge the user-defined application options with the default optionsthis.options={bottomPanel: {
...ApplicationShell.DEFAULT_OPTIONS.bottomPanel,
...options.bottomPanel||{}},leftPanel: {
...ApplicationShell.DEFAULT_OPTIONS.leftPanel,
...options.leftPanel||{}},rightPanel: {
...ApplicationShell.DEFAULT_OPTIONS.rightPanel,
...options.rightPanel||{}}};this.mainPanel=this.createMainPanel();this.topPanel=this.createTopPanel();this.bottomPanel=this.createBottomPanel();this.leftPanelHandler=sidePanelHandlerFactory();this.leftPanelHandler.create('left',this.options.leftPanel);this.leftPanelHandler.dockPanel.widgetAdded.connect((_,widget)=>this.fireDidAddWidget(widget));this.leftPanelHandler.dockPanel.widgetRemoved.connect((_,widget)=>this.fireDidRemoveWidget(widget));this.rightPanelHandler=sidePanelHandlerFactory();this.rightPanelHandler.create('right',this.options.rightPanel);this.rightPanelHandler.dockPanel.widgetAdded.connect((_,widget)=>this.fireDidAddWidget(widget));this.rightPanelHandler.dockPanel.widgetRemoved.connect((_,widget)=>this.fireDidRemoveWidget(widget));this.layout=this.createLayout();this.tracker.currentChanged.connect(this.onCurrentChanged,this);this.tracker.activeChanged.connect(this.onActiveChanged,this);}/** * Assemble the application shell layout. Override this method in order to change the arrangement * of the main area and the side panels. Layout 创建 */protectedcreateLayout(): Layout{constbottomSplitLayout=this.createSplitLayout([this.mainPanel,this.bottomPanel],[1,0],{orientation: 'vertical',spacing: 0});constpanelForBottomArea=newSplitPanel({layout: bottomSplitLayout});panelForBottomArea.id='theia-bottom-split-panel';constleftRightSplitLayout=this.createSplitLayout([this.leftPanelHandler.container,panelForBottomArea,this.rightPanelHandler.container],[0,1,0],{orientation: 'horizontal',spacing: 0});constpanelForSideAreas=newSplitPanel({layout: leftRightSplitLayout});panelForSideAreas.id='theia-left-right-split-panel';returnthis.createBoxLayout([this.topPanel,panelForSideAreas,this.statusBar],[0,1,0],{direction: 'top-to-bottom',spacing: 0});}/** * Create the dock panel in the main shell area. Panel 创建 */protectedcreateMainPanel(): TheiaDockPanel{constrenderer=this.dockPanelRendererFactory();renderer.tabBarClasses.push(MAIN_BOTTOM_AREA_CLASS);renderer.tabBarClasses.push(MAIN_AREA_CLASS);constdockPanel=newTheiaDockPanel({mode: 'multiple-document',
renderer,spacing: 0});dockPanel.id=MAIN_AREA_ID;dockPanel.widgetAdded.connect((_,widget)=>this.fireDidAddWidget(widget));dockPanel.widgetRemoved.connect((_,widget)=>this.fireDidRemoveWidget(widget));returndockPanel;}}
/******************************************************************************** * Copyright (C) 2018 TypeFox and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at * http://www.eclipse.org/legal/epl-2.0. * * This Source Code may also be made available under the following Secondary * Licenses when the conditions for such availability set forth in the Eclipse * Public License v. 2.0 are satisfied: GNU General Public License, version 2 * with the GNU Classpath Exception which is available at * https://www.gnu.org/software/classpath/license.html. * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/import*asReactDOMfrom'react-dom';import*asReactfrom'react';import{injectable,unmanaged}from'inversify';import{DisposableCollection,Disposable}from'../../common';import{BaseWidget,Message}from'./widget';import{Widget}from'@phosphor/widgets';
@injectable()exportabstractclassReactWidgetextendsBaseWidget{protectedreadonlyonRender=newDisposableCollection();constructor(@unmanaged()options?: Widget.IOptions){super(options);this.scrollOptions={suppressScrollX: true,minScrollbarLength: 35,};this.toDispose.push(Disposable.create(()=>{ReactDOM.unmountComponentAtNode(this.node);}));}protectedonUpdateRequest(msg: Message): void{super.onUpdateRequest(msg);ReactDOM.render(<React.Fragment>{this.render()}</React.Fragment>,this.node,()=>this.onRender.dispose());}/** * Render the React widget in the DOM. * - If the widget has been previously rendered, * any subsequent calls will perform an update and only * change the DOM if absolutely necessary. */protectedabstractrender(): React.ReactNode;}
import*asPropTypesfrom"prop-types";import*asReactfrom"react";import{createPortal}from"react-dom";import{Widget}from"@phosphor/widgets/lib/widget";import{Title}from"@phosphor/widgets/lib/title";require("@phosphor/widgets/style/widget.css");import{WidgetParentContext,IWidgetParent}from"./Common";exportinterfaceIWidgetProps{title?: Partial<Title.IOptions<Widget>>;}exportdefaultclassReactWidgetextendsReact.PureComponent<IWidgetProps,{}>{privatewidget: Widget;// TODO: aah why isn't this working// Some indication that this may be unstable (i.e. worked on 16.6.3 but not 16.6.1)// https://stackoverflow.com/questions/53110121/react-new-context-api-not-working-with-class-contexttype-but-works-with-contestaticcontextType=WidgetParentContext;contextType=WidgetParentContext;privatestoredContext: IWidgetParent;constructor(props){super(props);this.widget=newWidget();ReactWidget.setTitleKeys(this.widget,{},props);}componentDidMount(){letparent=this.storedContext;if(!parent)thrownewError("ReactWidget must be wrapped in a container component (BoxPanel, SplitPanel, etc.)");parent.receiveChild(this.widget);}componentDidUpdate(prevProps: IWidgetProps){ReactWidget.setTitleKeys(this.widget,prevProps,this.props);}staticsetTitleKeys(widget: Widget,prevProps: IWidgetProps,props: IWidgetProps){lettitleKeys: (keyofTitle.IOptions<Widget>)[]=["caption","className","closable","dataset","icon","iconClass","iconLabel","label","mnemonic"];for(letkoftitleKeys){if((prevProps.title||{})[kasany]!==(props.title||{})[kasany]){widget.title[kasany]=props.title[kasany];}}}render(){returncreatePortal(<div><p><WidgetParentContext.Consumer>{(value)=>{this.storedContext=value;returnnull;}}</WidgetParentContext.Consumer></p>{this.props.children}</div>,this.widget.node);}}
FrontendApplication.start()
构建的Theia 框架前端 UI 布局和 Services 一样,具备灵活可拓展的特点。VSCode 是内置了一套基本的组件系统,而 Theia 框架的 UI 布局基于 PhosphorJS 框架。 PhosphorJS 提供了包含 widgets、layouts、事件和数据结构的丰富工具包。这使得开发人员能够构建可扩展的、高性能的、类桌面的 Web 应用程序,比如 JupyterLab。
效果
example-dockpanel
在 PhosphorJS 里运行 React 代码:ermalism/phosphorjs-react-jsx-example
Widget
PhosphorJS 布局的核心就在于 Widget。
这里的 Widget 和 Flutter 里面的 Widget 还不一样,Flutter 的 Widget 属于声明式 UI(declarative UI),而 PhosphorJS 的 Widget 更像是命令式 UI(imperative UI)。和 Chrome 开发者工具 ChromeDevTools/devtools-frontend 的 Widget 更类似。
关于声明式和命令式 UI 框架也可以阅读:聊聊我对现代前端框架的认知 作为补充。
Widget 的继承
官方提供了一系列 Widget 的继承实现:
Widget
并且都实现了 IDisposable 和 IMessageHandler 接口。
接口
Widget 包含以下状态:isDisposed、isAttached、isHidden、isVisible,以及一系列事件驱动的钩子:onCloseRequest、onResize、onUpdateRequest、onFitRequest、onActivateRequest、onBeforeShow、onBeforeHide、onBeforeAttach、onBeforeDetach、onChildAdded 等。
渲染的核心的方法在于
Widget.attach
,本质上就是:host.insertBefore(widget.node, ref);
。Widget 主要的字段及接口如下:
Theia 布局构建
Theia 前端页面的启动非常简单:
可以看到核心就在于
FrontendApplication.start()
方法,那么这个方法里做了什么?FrontendApplication
ApplicationShell
主要分为:mainPanel:TheiaDockPanel、topPanel:Panel、bottomPanel:TheiaDockPanel、leftPanel、rightPanel
在 ApplicationShell 中初始化并拼装。
Plugin API 里的 Widget 创建
Node/Browser API 的 Widget 创建:通过 WidgetFactory。
Packages
commands
Class CommandRegistry 管理命令集合的对象。用于 CommandRegistry 类 statics 的命名空间。
命令注册表可用于填充各种 action-based widgets,如命令 palettes、menus 和 toolbars。
Widgets 与 React
将 React 组件封装成 Widget 组件
思路:
然后当作自定义的 Widget 使用即可。
Theia 已提供抽象组件 ReactWidgt 供参考:packages/core/src/browser/widgets/react-widget.tsx
将 Widget 封装成 React 组件
或者参考:Run a PhosphorJS DockerPanel with Widgets INSIDE a React component
参考
The text was updated successfully, but these errors were encountered: