Optimize libsui js-sdk loading logic

This commit is contained in:
Max 2024-08-03 08:37:54 +08:00
parent a07c1ece51
commit 5f5af44018
13 changed files with 392 additions and 352 deletions

View file

@ -130,7 +130,8 @@ bindata:
cp -r ui .tmp/data/public
cp -r xgen .tmp/data/
cp -r yao .tmp/data/
cp -r builder .tmp/data/
cp -r sui/libsui .tmp/data/
find .tmp/data -name ".DS_Store" -type f -delete
go-bindata -fs -pkg data -o data/bindata.go -prefix ".tmp/data/" .tmp/data/...
rm -rf .tmp/data
rm -rf .tmp/yao-init

View file

@ -1,3 +0,0 @@
function foo() {
return "foo";
}

View file

@ -1 +0,0 @@
<div>Yao Builder</div>

File diff suppressed because one or more lines are too long

View file

@ -46,18 +46,6 @@ func Setup() *assetfs.AssetFS {
panic("unreachable")
}
// Builder Builder ui
func Builder() *assetfs.AssetFS {
assetInfo := func(path string) (os.FileInfo, error) {
return os.Stat(path)
}
for k := range _bintree.Children {
k = "builder"
return &assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, AssetInfo: assetInfo, Prefix: k, Fallback: "index.html"}
}
panic("unreachable")
}
// ReplaceXGen bindata file
func ReplaceXGen(search, replace string) error {
err := replaceXGenIndex(search, replace)

View file

@ -18,9 +18,6 @@ var AppFileServer http.Handler
// XGenFileServerV1 XGen v1.0
var XGenFileServerV1 http.Handler = http.FileServer(data.XgenV1())
// BuilderFileServer Builder ui
var BuilderFileServer http.Handler = http.FileServer(data.Builder())
// AdminRoot cache
var AdminRoot = ""

View file

@ -71,6 +71,19 @@ func (page *Page) Build(ctx *BuildContext, option *BuildOption) (*goquery.Docume
ctx.warnings = append(ctx.warnings, warnings...)
}
// Prepend the libsui.min.js
if !option.IgnoreLibSUI {
script := ScriptNode{
Parent: "head",
Attrs: []html.Attribute{
{Key: "src", Val: fmt.Sprintf("%s/libsui.min.js", option.AssetRoot)},
{Key: "type", Val: "text/javascript"},
{Key: "name", Val: "libsui"},
},
}
ctx.scripts = append([]ScriptNode{script}, ctx.scripts...)
}
// Scripts
scripts, err := page.BuildScripts(ctx, option, "__page", namespace)
if err != nil {

View file

@ -58,13 +58,6 @@ func (page *Page) Compile(ctx *BuildContext, option *BuildOption) (string, []str
}
// SUI lib
lib, err := libsui(option.ScriptMinify)
if err != nil {
return "", warnings, err
}
head.AppendHtml("\n\n" + `<script name="sui" type="text/javascript">` + lib + `</script>` + "\n\n")
// Page Config
page.Config = page.GetConfig()

View file

@ -5,235 +5,39 @@ import (
"github.com/evanw/esbuild/pkg/api"
"github.com/yaoapp/gou/runtime/transform"
"github.com/yaoapp/yao/data"
)
var libsuicode = ""
func libsui(minify bool) (string, error) {
if libsuicode != "" {
return libsuicode, nil
}
// LibSUI return the libsui code
func LibSUI() ([]byte, []byte, error) {
option := api.TransformOptions{Target: api.ES2015}
if minify {
option.MinifyIdentifiers = true
option.MinifySyntax = true
option.MinifyWhitespace = true
}
var err error
libsuicode, err = transform.JavaScript(libsuisource, option)
// Read source code from bindata
index, err := data.Read("libsui/index.ts")
if err != nil {
return "", fmt.Errorf("libsui error: %w", err)
return nil, nil, err
}
return libsuicode, nil
utils, err := data.Read("libsui/utils.ts")
if err != nil {
return nil, nil, err
}
// Merge the source code
source := fmt.Sprintf("%s\n%s", index, utils)
// Build the source code
js, sm, err := transform.TypeScriptWithSourceMap(string(source), api.TransformOptions{
Target: api.ES2015,
MinifyIdentifiers: true,
MinifySyntax: true,
MinifyWhitespace: true,
})
return js, sm, nil
}
const libsuisource = `
function $$(selector) {
elm = null;
if (typeof selector === "string" ){
elm = document.querySelector(selector);
}
if (selector instanceof HTMLElement) {
elm = selector;
}
if (elm) {
cn = elm.getAttribute("s:cn");
if (cn != "" && typeof window[cn] === "function") {
const component = new window[cn](elm);
return new __sui_component(elm, component);
}
}
return null;
}
const $utils = {
Store: (elm) => {
if (typeof elm === "string") {
elm = document.querySelector(elm);
}
return new __sui_store(elm);
},
RemoveClass: (element, className) => {
const classes = Array.isArray(className) ? className : className.split(" ");
classes.forEach((c) => {
const v = c.replace(/[\n\r\s]/g, "");
if (v === "") return;
element.classList.remove(v);
});
return $utils;
},
AddClass: (element, className) => {
const classes = Array.isArray(className) ? className : className.split(" ");
classes.forEach((c) => {
const v = c.replace(/[\n\r\s]/g, "");
if (v === "") return;
element.classList.add(v);
});
return $utils;
},
}
function __sui_component_root(elm, name) {
while (elm && elm.getAttribute("s:cn") !== name) {
elm = elm.parentElement;
}
return elm;
}
function __sui_state(component) {
this.handlers = component.watch || {};
this.Set = async function (key, value, target) {
const handler = this.handlers[key];
target = target || component.root;
if (handler && typeof handler === "function") {
const stateObj = {
target: target,
stopPropagation: function () {
target.setAttribute("state-propagation", "true");
},
}
await handler(value, stateObj);
const isStopPropagation = target ? target.getAttribute("state-propagation") === "true" : false;
if (isStopPropagation) {
return;
}
let parent = component.root.parentElement;
while (parent && !parent.getAttribute("s:cn")) {
parent = parent.parentElement;
}
if ( parent == document.body || parent == null) {
return;
}
// Dispatch the state change custom event to parent component
const event = new CustomEvent("state:change", { detail: { key: key, value: value, target:component.root } });
parent.dispatchEvent(event);
}
}
}
function __sui_props(elm) {
this.Get = function (key) {
if (!elm || typeof elm.getAttribute !== "function") {
return null;
}
const k = "prop:" + key;
const v = elm.getAttribute(k);
const json = elm.getAttribute("json-attr-prop:" + key) === "true";
if (json) {
try {
return JSON.parse(v);
} catch (e) {
return null;
}
}
return v;
}
this.List = function () {
const props = {};
if (!elm || typeof elm.getAttribute !== "function") {
return props;
}
const attrs = elm.attributes;
for (let i = 0; i < attrs.length; i++) {
const attr = attrs[i];
if (attr.name.startsWith("prop:")) {
const k = attr.name.replace("prop:", "");
const json = elm.getAttribute("json-attr-prop:" + k) === "true";
if (json) {
try {
props[k] = JSON.parse(attr.value);
} catch (e) {
props[k] = null;
}
continue;
}
props[k] = attr.value;
}
}
return props;
}
}
function __sui_component(elm, component) {
this.root = elm;
this.store = new __sui_store(elm);
this.props = new __sui_props(elm);
this.state = component ? new __sui_state(component) : {};
}
function __sui_event_handler(event, dataKeys, jsonKeys, target, root, handler) {
const data = {};
target = target || null;
if (target) {
dataKeys.forEach(function (key) {
const value = target.getAttribute("data:" + key);
data[key] = value;
})
jsonKeys.forEach(function (key) {
const value = target.getAttribute("json:" + key);
data[key] = null;
if (value && value != "") {
try {
data[key] = JSON.parse(value);
} catch (e) {
const message = e.message || e || "An error occurred";
console.error(` + "`[SUI] Event Handler Error: ${message}`" + `, target);
}
}
})
}
handler && handler(event, data, {
rootElement: root,
targetElement: target
});
};
function __sui_store(elm) {
elm = elm || document.body;
this.Get = function (key) {
return elm.getAttribute("data:" + key);
}
this.Set = function (key, value) {
elm.setAttribute("data:" + key, value);
}
this.GetJSON = function (key) {
const value = elm.getAttribute("json:" + key);
if (value && value != "") {
try {
const res = JSON.parse(value);
return res;
} catch (e) {
const message = e.message || e || "An error occurred";
console.error(` + "`[SUI] Event Handler Error: ${message}`" + `, elm);
return null;
}
}
return null;
}
this.SetJSON = function (key, value) {
elm.setAttribute("json:" + key, JSON.stringify(value));
}
this.GetData = function () {
return this.GetJSON("__component_data") || {};
}
}
`
const initScriptTmpl = `
try {
var __sui_data = %s;

View file

@ -262,6 +262,7 @@ type BuildOption struct {
PublicRoot string `json:"public_root,omitempty"`
AssetRoot string `json:"asset_root,omitempty"`
IgnoreAssetRoot bool `json:"ignore_asset_root,omitempty"`
IgnoreLibSUI bool `json:"ignore_lib_sui,omitempty"`
IgnoreDocument bool `json:"ignore_document,omitempty"`
JitMode bool `json:"jit_mode,omitempty"`
WithWrapper bool `json:"with_wrapper,omitempty"`

177
sui/libsui/index.ts Normal file
View file

@ -0,0 +1,177 @@
function $$(selector) {
let elm: HTMLElement | null = null;
if (typeof selector === "string") {
elm = document.querySelector(selector);
}
if (selector instanceof HTMLElement) {
elm = selector;
}
if (elm) {
const cn = elm.getAttribute("s:cn");
if (cn && cn != "" && typeof window[cn] === "function") {
const component = new window[cn](elm);
return new __sui_component(elm, component);
}
}
return null;
}
function __sui_component_root(elm, name) {
while (elm && elm.getAttribute("s:cn") !== name) {
elm = elm.parentElement;
}
return elm;
}
function __sui_state(component) {
this.handlers = component.watch || {};
this.Set = async function (key, value, target) {
const handler = this.handlers[key];
target = target || component.root;
if (handler && typeof handler === "function") {
const stateObj = {
target: target,
stopPropagation: function () {
target.setAttribute("state-propagation", "true");
},
};
await handler(value, stateObj);
const isStopPropagation = target
? target.getAttribute("state-propagation") === "true"
: false;
if (isStopPropagation) {
return;
}
let parent = component.root.parentElement;
while (parent && !parent.getAttribute("s:cn")) {
parent = parent.parentElement;
}
if (parent == document.body || parent == null) {
return;
}
// Dispatch the state change custom event to parent component
const event = new CustomEvent("state:change", {
detail: { key: key, value: value, target: component.root },
});
parent.dispatchEvent(event);
}
};
}
function __sui_props(elm) {
this.Get = function (key) {
if (!elm || typeof elm.getAttribute !== "function") {
return null;
}
const k = "prop:" + key;
const v = elm.getAttribute(k);
const json = elm.getAttribute("json-attr-prop:" + key) === "true";
if (json) {
try {
return JSON.parse(v);
} catch (e) {
return null;
}
}
return v;
};
this.List = function () {
const props = {};
if (!elm || typeof elm.getAttribute !== "function") {
return props;
}
const attrs = elm.attributes;
for (let i = 0; i < attrs.length; i++) {
const attr = attrs[i];
if (attr.name.startsWith("prop:")) {
const k = attr.name.replace("prop:", "");
const json = elm.getAttribute("json-attr-prop:" + k) === "true";
if (json) {
try {
props[k] = JSON.parse(attr.value);
} catch (e) {
props[k] = null;
}
continue;
}
props[k] = attr.value;
}
}
return props;
};
}
function __sui_component(elm, component) {
this.root = elm;
this.store = new __sui_store(elm);
this.props = new __sui_props(elm);
this.state = component ? new __sui_state(component) : {};
}
function __sui_event_handler(event, dataKeys, jsonKeys, target, root, handler) {
const data = {};
target = target || null;
if (target) {
dataKeys.forEach(function (key) {
const value = target.getAttribute("data:" + key);
data[key] = value;
});
jsonKeys.forEach(function (key) {
const value = target.getAttribute("json:" + key);
data[key] = null;
if (value && value != "") {
try {
data[key] = JSON.parse(value);
} catch (e) {
const message = e.message || e || "An error occurred";
console.error(`[SUI] Event Handler Error: ${message} `, target);
}
}
});
}
handler &&
handler(event, data, {
rootElement: root,
targetElement: target,
});
}
function __sui_store(elm) {
elm = elm || document.body;
this.Get = function (key) {
return elm.getAttribute("data:" + key);
};
this.Set = function (key, value) {
elm.setAttribute("data:" + key, value);
};
this.GetJSON = function (key) {
const value = elm.getAttribute("json:" + key);
if (value && value != "") {
try {
const res = JSON.parse(value);
return res;
} catch (e) {
const message = e.message || e || "An error occurred";
console.error(`[SUI] Event Handler Error: ${message}`, elm);
return null;
}
}
return null;
};
this.SetJSON = function (key, value) {
elm.setAttribute("json:" + key, JSON.stringify(value));
};
this.GetData = function () {
return this.GetJSON("__component_data") || {};
};
}

28
sui/libsui/utils.ts Normal file
View file

@ -0,0 +1,28 @@
const $utils = {
Store: (elm) => {
if (typeof elm === "string") {
elm = document.querySelector(elm);
}
return new __sui_store(elm);
},
RemoveClass: (element, className) => {
const classes = Array.isArray(className) ? className : className.split(" ");
classes.forEach((c) => {
const v = c.replace(/[\n\r\s]/g, "");
if (v === "") return;
element.classList.remove(v);
});
return $utils;
},
AddClass: (element, className) => {
const classes = Array.isArray(className) ? className : className.split(" ");
classes.forEach((c) => {
const v = c.replace(/[\n\r\s]/g, "");
if (v === "") return;
element.classList.add(v);
});
return $utils;
},
};

View file

@ -124,6 +124,12 @@ func (tmpl *Template) Build(option *core.BuildOption) ([]string, error) {
return warnings, err
}
// Add sui lib to the global
err = tmpl.UpdateJSSDK(option)
if err != nil {
return warnings, err
}
// Execute the build after hook
if option.ExecScripts {
res := tmpl.ExecAfterBuildScripts()
@ -200,6 +206,42 @@ func (tmpl *Template) SyncAssetFile(file string, option *core.BuildOption) error
return copy(sourceFile, targetFile)
}
// UpdateJSSDK update the js sdk
func (tmpl *Template) UpdateJSSDK(option *core.BuildOption) error {
jsCode, sourceMap, err := core.LibSUI()
if err != nil {
return err
}
// get source abs path
root, err := tmpl.local.DSL.PublicRoot(option.Data)
if err != nil {
log.Error("SyncAssets: Get the public root error: %s. use %s", err.Error(), tmpl.local.DSL.Public.Root)
root = tmpl.local.DSL.Public.Root
}
targetRoot := filepath.Join(application.App.Root(), "public", root, "assets")
file := filepath.Join(targetRoot, "libsui.min.js")
mapFile := filepath.Join(targetRoot, "libsui.min.js.map")
// create the target directory
if exist, _ := os.Stat(targetRoot); exist == nil {
os.MkdirAll(targetRoot, os.ModePerm)
}
// write the js sdk
err = os.WriteFile(file, []byte(jsCode), 0644)
if err != nil {
return err
}
// write the source map
err = os.WriteFile(mapFile, []byte(sourceMap), 0644)
return nil
}
func (tmpl *Template) writeGlobalScript(data map[string]interface{}) error {
file, source, err := tmpl.backendScriptSource("__global.backend")
if err != nil {