Browse Source

Use only the dependencies necessary for an element.

Dominique CLAUSE 2 years ago
parent
commit
fc7e02e99b

+ 4 - 4
nicegui/client.py

@@ -11,7 +11,7 @@ from fastapi.templating import Jinja2Templates
 from nicegui import json
 
 from . import __version__, globals, outbox
-from .dependencies import generate_js_imports, generate_vue_content
+from .dependencies import generate_resources
 from .element import Element
 from .favicon import get_favicon_url
 
@@ -67,17 +67,17 @@ class Client:
 
     def build_response(self, request: Request, status_code: int = 200) -> Response:
         prefix = request.headers.get('X-Forwarded-Prefix', '')
-        vue_html, vue_styles, vue_scripts = generate_vue_content()
         elements = json.dumps({id: element._to_dict() for id, element in self.elements.items()})
+        (vue_html, vue_styles, vue_scripts, es5_exposes, js_imports) = generate_resources(prefix, self.elements.values())
         return templates.TemplateResponse('index.html', {
             'request': request,
             'version': __version__,
             'client_id': str(self.id),
             'elements': elements,
             'head_html': self.head_html,
-            'body_html': f'{self.body_html}\n{vue_html}\n{vue_styles}',
+            'body_html': f'{vue_styles}\n{self.body_html}\n{vue_html}',
             'vue_scripts': vue_scripts,
-            'js_imports': generate_js_imports(prefix),
+            'js_imports': js_imports,
             'title': self.page.resolve_title(),
             'viewport': self.page.resolve_viewport(),
             'favicon_url': get_favicon_url(self.page, prefix),

+ 68 - 71
nicegui/dependencies.py

@@ -1,86 +1,83 @@
-from dataclasses import dataclass
+import warnings
 from pathlib import Path
-from typing import Dict, List, Set, Tuple
+from typing import Any, Dict, List, Tuple
 
 import vbuild
 
-from . import __version__, globals
-from .ids import IncrementingStringIds
+from . import __version__
 
+js_dependencies: Dict[str, Any] = {}  # @todo remove when unused in elements.
+vue_components: Dict[str, Any] = {}
+js_components: Dict[str, Path] = {}
+libraries: Dict[str, Any] = {}
 
-@dataclass
-class Component:
-    name: str
-    path: Path
-
-    @property
-    def import_path(self) -> str:
-        return f'/_nicegui/{__version__}/components/{self.name}'
-
-
-@dataclass
-class Dependency:
-    id: int
-    path: Path
-    dependents: Set[str]
-    optional: bool
-
-    @property
-    def import_path(self) -> str:
-        return f'/_nicegui/{__version__}/dependencies/{self.id}/{self.path.name}'
 
+def register_vue_component(name: str, path: Path) -> None:
+    """
+    Register a .vue or .js vue component.
+    The component (in case of .vue) is built right away to:
+        1. delegate this 'long' process to the bootstrap phase
+        2. avoid building the component on every single requests
+    """
+    suffix = path.suffix.lower()
+    assert suffix in {'.vue', '.js'}, 'Only VUE and JS components are supported.'
+    if suffix == '.vue':
+        assert name not in vue_components, f'Duplicate VUE component name {name}'
+        vue_components[name] = vbuild.VBuild(name, path.read_text())
+    elif suffix == '.js':
+        assert name not in js_components, f'Duplicate JS component name {name}'
+        js_components[name] = path
 
-dependency_ids = IncrementingStringIds()
 
-vue_components: Dict[str, Component] = {}
-js_components: Dict[str, Component] = {}
-js_dependencies: Dict[int, Dependency] = {}
+def register_library(name: str, path: Path) -> None:
+    """
+    Register a  new external library.
+    :param name: the library unique name (used in component `use_library`).
+    :param path: the library local path.
+    """
+    assert path.suffix == '.js', 'Only JS dependencies are supported.'
+    libraries[name] = {'path': path}
 
 
 def register_component(name: str, py_filepath: str, component_filepath: str, dependencies: List[str] = [],
                        optional_dependencies: List[str] = []) -> None:
+    """
+    Deprecated method. Use `register_vue_component` or `register_library` library instead.
+    """
+
+    url = f'https://github.com/zauberzeug/nicegui/pull/xxx'  # @todo to be defined.
+    warnings.warn(DeprecationWarning(
+        f'This function is deprecated. Use either register_vue_component or register_library instead, along with `use_component` or `use_library` ({url}).'))
+
     suffix = Path(component_filepath).suffix.lower()
-    assert suffix in {'.vue', '.js'}, 'Only VUE and JS components are supported.'
-    if suffix == '.vue':
-        assert name not in vue_components, f'Duplicate VUE component name {name}'
-        vue_components[name] = Component(name=name, path=Path(py_filepath).parent / component_filepath)
-    elif suffix == '.js':
-        assert name not in js_components, f'Duplicate JS component name {name}'
-        js_components[name] = Component(name=name, path=Path(py_filepath).parent / component_filepath)
+    if suffix in {'.vue', '.js'}:
+        register_vue_component(name, Path(Path(py_filepath).parent, component_filepath).absolute())
+
     for dependency in dependencies + optional_dependencies:
-        path = Path(py_filepath).parent / dependency
-        assert path.suffix == '.js', 'Only JS dependencies are supported.'
-        id = dependency_ids.get(str(path.resolve()))
-        if id not in js_dependencies:
-            optional = dependency in optional_dependencies
-            js_dependencies[id] = Dependency(id=id, path=path, dependents=set(), optional=optional)
-        js_dependencies[id].dependents.add(name)
-
-
-def generate_vue_content() -> Tuple[str, str, str]:
-    builds = [
-        vbuild.VBuild(name, component.path.read_text())
-        for name, component in vue_components.items()
-        if name not in globals.excludes
-    ]
-    return (
-        '\n'.join(v.html for v in builds),
-        '<style>' + '\n'.join(v.style for v in builds) + '</style>',
-        '\n'.join(v.script.replace('Vue.component', 'app.component', 1) for v in builds),
-    )
-
-
-def generate_js_imports(prefix: str) -> str:
-    result = ''
-    for dependency in js_dependencies.values():
-        if dependency.optional:
-            continue
-        if not dependency.dependents.difference(globals.excludes):
-            continue
-        result += f'import "{prefix}{dependency.import_path}";\n'
-    for name, component in js_components.items():
-        if name in globals.excludes:
-            continue
-        result += f'import {{ default as {name} }} from "{prefix}{component.import_path}";\n'
-        result += f'app.component("{name}", {name});\n'
-    return result
+        path = Path(Path(py_filepath).parent, dependency)
+        register_library(name, path)
+
+
+def generate_resources(prefix: str, elements) -> Tuple[str, str, str, str, str]:
+    vue_scripts = ''
+    vue_html = ''
+    vue_styles = ''
+    js_imports = ''
+    es5_exposes = ''
+
+    # Build the resources associated with the elements.
+    for element in elements:
+        for name in element.components:
+            if name in vue_components:
+                vue_html += f'{vue_components[name].html}\n'
+                vue_scripts += f'{vue_components[name].script.replace("Vue.component", "app.component", 1)}\n'
+                vue_styles += f'{vue_components[name].style}\n'
+            if name in js_components:
+                js_imports += f'import {{ default as {name} }} "{prefix}{js_components[name]}";\n'
+                js_imports += f'app.component("{name}", {name});\n'
+        for name in element.libraries:
+            if name in libraries:
+                js_imports += f'import "{prefix}/_nicegui/{__version__}/library/{name}";\n'
+
+    vue_styles = f'<style>{vue_styles}</style>'
+    return vue_html, vue_styles, vue_scripts, es5_exposes, js_imports

+ 8 - 0
nicegui/element.py

@@ -42,6 +42,8 @@ class Element(Visibility):
         self._props: Dict[str, Any] = {}
         self._event_listeners: Dict[str, EventListener] = {}
         self._text: str = ''
+        self.components: List[str] = []
+        self.libraries: List[str] = []
         self.slots: Dict[str, Slot] = {}
         self.default_slot = self.add_slot('default')
 
@@ -268,3 +270,9 @@ class Element(Visibility):
 
         Can be overridden to perform cleanup.
         """
+
+    def use_component(self, name: str) -> None:
+        self.components.append(name)
+
+    def use_library(self, name: str) -> None:
+        self.libraries.append(name)

+ 7 - 3
nicegui/elements/plotly.py

@@ -1,11 +1,14 @@
+from pathlib import Path
 from typing import Dict, Union
 
 import plotly.graph_objects as go
 
-from ..dependencies import js_dependencies, register_component
+from ..dependencies import register_vue_component, register_library
 from ..element import Element
 
-register_component('plotly', __file__, 'plotly.vue', [], ['lib/plotly.min.js'])
+
+register_vue_component(name='plotly', path=Path(__file__).parent.joinpath('plotly.vue'))
+register_library(name='plotly', path=Path(__file__).parent.joinpath('lib', 'plotly', 'plotly.min.js'))
 
 
 class Plotly(Element):
@@ -28,7 +31,8 @@ class Plotly(Element):
         super().__init__('plotly')
 
         self.figure = figure
-        self._props['lib'] = [d.import_path for d in js_dependencies.values() if d.path.name == 'plotly.min.js'][0]
+        self.use_component('plotly')
+        self.use_library('plotly')
         self.update()
 
     def update_figure(self, figure: Union[Dict, go.Figure]):

+ 18 - 32
nicegui/elements/plotly.vue

@@ -5,29 +5,25 @@
 <script>
 export default {
   mounted() {
-    setTimeout(() => {
-      this.ensureLibLoaded().then(() => {
-        // initial rendering of chart
-        Plotly.newPlot(this.$el.id, this.options.data, this.options.layout, this.options.config);
+    // initial rendering of chart
+    Plotly.newPlot(this.$el.id, this.options.data, this.options.layout, this.options.config);
 
-        // register resize observer on parent div to auto-resize Plotly chart
-        const doResize = () => {
-          // only call resize if actually visible, otherwise error in Plotly.js internals
-          if (this.isHidden(this.$el)) return;
-          Plotly.Plots.resize(this.$el);
-        };
+    // register resize observer on parent div to auto-resize Plotly chart
+    const doResize = () => {
+      // only call resize if actually visible, otherwise error in Plotly.js internals
+      if (this.isHidden(this.$el)) return;
+      Plotly.Plots.resize(this.$el);
+    };
 
-        // throttle Plotly resize calls for better performance
-        // using HTML5 ResizeObserver on parent div
-        this.resizeObserver = new ResizeObserver((entries) => {
-          if (this.timeoutHandle) {
-            clearTimeout(this.timeoutHandle);
-          }
-          this.timeoutHandle = setTimeout(doResize, this.throttleResizeMs);
-        });
-        this.resizeObserver.observe(this.$el);
-      });
-    }, 0); // NOTE: wait for window.path_prefix to be set in app.mounted()
+    // throttle Plotly resize calls for better performance
+    // using HTML5 ResizeObserver on parent div
+    this.resizeObserver = new ResizeObserver((entries) => {
+      if (this.timeoutHandle) {
+        clearTimeout(this.timeoutHandle);
+      }
+      this.timeoutHandle = setTimeout(doResize, this.throttleResizeMs);
+    });
+    this.resizeObserver.observe(this.$el);
   },
   unmounted() {
     this.resizeObserver.disconnect();
@@ -43,17 +39,8 @@ export default {
       return !display || display === "none";
     },
 
-    ensureLibLoaded() {
-      // ensure Plotly imported (lazy-load)
-      return import(window.path_prefix + this.lib);
-    },
-
     update(options) {
-      // ensure Plotly imported, otherwise first plot will fail in update call
-      // because library not loaded yet
-      this.ensureLibLoaded().then(() => {
-        Plotly.newPlot(this.$el.id, options.data, options.layout, options.config);
-      });
+      Plotly.newPlot(this.$el.id, options.data, options.layout, options.config);
     },
   },
 
@@ -67,7 +54,6 @@ export default {
 
   props: {
     options: Object,
-    lib: String,
   },
 };
 </script>

+ 8 - 1
nicegui/nicegui.py

@@ -16,7 +16,7 @@ from nicegui.json import NiceGUIJSONResponse
 from . import __version__, background_tasks, binding, globals, outbox
 from .app import App
 from .client import Client
-from .dependencies import js_components, js_dependencies
+from .dependencies import js_components, js_dependencies, libraries
 from .element import Element
 from .error import error_content
 from .helpers import safe_invoke
@@ -54,6 +54,13 @@ def get_dependencies(id: int, name: str):
     raise HTTPException(status_code=404, detail=f'dependency "{name}" with ID {id} not found')
 
 
+@app.get(f'/_nicegui/{__version__}' + '/library/{name}')
+def get_dependencies(name: str):
+    if name in libraries and libraries[name]['path'].exists():
+        return FileResponse(libraries[name]['path'], media_type='text/javascript')
+    raise HTTPException(status_code=404, detail=f'dependency "{name}" not found')
+
+
 @app.get(f'/_nicegui/{__version__}' + '/components/{name}')
 def get_components(name: str):
     return FileResponse(js_components[name].path, media_type='text/javascript')

+ 4 - 3
nicegui/templates/index.html

@@ -3,16 +3,17 @@
   <head>
     <title>{{ title }}</title>
     <meta name="viewport" content="{{ viewport }}" />
-    <script src="{{ prefix | safe }}/_nicegui/{{version}}/static/socket.io.min.js"></script>
     <link href="{{ favicon_url }}" rel="shortcut icon" />
     <link href="{{ prefix | safe }}/_nicegui/{{version}}/static/nicegui.css" rel="stylesheet" type="text/css" />
     <link href="{{ prefix | safe }}/_nicegui/{{version}}/static/fonts.css" rel="stylesheet" type="text/css" />
     <link href="{{ prefix | safe }}/_nicegui/{{version}}/static/quasar.prod.css" rel="stylesheet" type="text/css" />
+  </head>
+  <body>
+    <script src="{{ prefix | safe }}/_nicegui/{{version}}/static/es-module-shims.js"></script>
+    <script src="{{ prefix | safe }}/_nicegui/{{version}}/static/socket.io.min.js"></script>
     {% if tailwind %}
     <script src="{{ prefix | safe }}/_nicegui/{{version}}/static/tailwindcss.min.js"></script>
     {% endif %} {{ head_html | safe }}
-  </head>
-  <body>
     <script src="{{ prefix | safe }}/_nicegui/{{version}}/static/vue.global.prod.js"></script>
     <script src="{{ prefix | safe }}/_nicegui/{{version}}/static/quasar.umd.prod.js"></script>
 

+ 1 - 1
pyproject.toml

@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "nicegui"
-version = "0.1.0"
+version = "1.3.0"
 description = "Web User Interface with Buttons, Dialogs, Markdown, 3D Scences and Plots"
 authors = ["Zauberzeug GmbH <info@zauberzeug.com>"]
 license = "MIT"