Merge pull request #713 from trheyi/main

Optimize SUI frontend and backend script data sharing
This commit is contained in:
Max 2024-07-26 19:43:15 +08:00 committed by GitHub
commit dbf2be8ce8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 261 additions and 334 deletions

View file

@ -133,14 +133,14 @@ func (r *Request) Render() (string, int, error) {
if c.Data != "" {
err = r.Request.ExecStringMerge(data, c.Data)
if err != nil {
return "", 500, fmt.Errorf("data error, please re-complie the page %s", err.Error())
return "", 500, fmt.Errorf("data error, please re-complie the page. %s", err.Error())
}
}
if c.Global != "" {
global, err := r.Request.ExecString(c.Global)
if err != nil {
return "", 500, fmt.Errorf("global data error, please re-complie the page %s", err.Error())
return "", 500, fmt.Errorf("global data error, please re-complie the page. %s", err.Error())
}
data["$global"] = global
}

View file

@ -341,7 +341,10 @@ func (page *Page) parseProps(from *goquery.Selection, to *goquery.Selection, ext
exp := dataTokens.MatchString(attr.Val)
prop := PageProp{Key: attr.Key, Val: attr.Val, Trans: trans, Exp: exp}
page.props[attr.Key] = prop
to.SetAttr(attr.Key, attr.Val)
// Set the prop
key := fmt.Sprintf("prop:%s", attr.Key)
to.SetAttr(key, attr.Val)
}
if extra != nil && len(extra) > 0 {

View file

@ -142,14 +142,10 @@ func (page *Page) CompileAsComponent(ctx *BuildContext, option *BuildOption) (st
return "", warnings, err
}
// Page Components
components := []string{}
rawComponents := "{}"
if ctx != nil && ctx.components != nil && len(ctx.components) > 0 {
for route := range ctx.components {
components = append(components, route)
}
rawComponents, _ = jsoniter.MarshalToString(ctx.components)
}
rawComponents, _ := jsoniter.MarshalToString(components)
if body.Children().Length() == 0 {
return "", warnings, fmt.Errorf("page %s as component should have one root element", page.Route)

View file

@ -42,7 +42,46 @@ const libsuisource = `
function __sui_props(elm) {
this.Get = function (key) {
return elm && elm.getAttribute(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;
}
}
@ -123,6 +162,10 @@ const libsuisource = `
this.SetJSON = function (key, value) {
elm.setAttribute("json:" + key, JSON.stringify(value));
}
this.GetData = function () {
return this.GetJSON("__component_data") || {};
}
}
`
@ -184,6 +227,7 @@ const compEventScriptTmpl = `
const componentInitScriptTmpl = `
this.root = %s;
this.store = new __sui_store(this.root);
this.props = new __sui_props(this.root);
`
// Inject code

View file

@ -8,6 +8,7 @@ import (
"strings"
"github.com/PuerkitoBio/goquery"
"github.com/hashicorp/go-multierror"
jsoniter "github.com/json-iterator/go"
"github.com/yaoapp/gou/application"
)
@ -19,6 +20,7 @@ type JitComponent struct {
html string
scripts []ScriptNode
styles []StyleNode
imports map[string]string
buildOption *BuildOption
}
@ -39,6 +41,7 @@ var chComp = make(chan *componentData, 1)
var reScripts = regexp.MustCompile(`<script[^>]*name="scripts"[^>]*>(.*?)</script>`)
var reStyles = regexp.MustCompile(`<script[^>]*name="styles"[^>]*>(.*?)</script>`)
var reOption = regexp.MustCompile(`<script[^>]*name="option"[^>]*>(.*?)</script>`)
var reImports = regexp.MustCompile(`<script[^>]*name="imports"[^>]*>(.*?)</script>`)
func init() {
go componentWriter()
@ -47,119 +50,222 @@ func init() {
// parseComponent parse the component
func (parser *TemplateParser) parseJitComponent(sel *goquery.Selection) {
parser.parsed(sel)
comp, props, slots, children, err := parser.getComponent(sel)
comp, err := parser.getJitComponent(sel)
if err != nil {
parser.errors = append(parser.errors, err)
setError(sel, err)
return
}
html, _, err := parser.RenderComponent(comp, props, slots, children)
comsel, err := parser.newJitComponentSel(sel, comp)
if err != nil {
parser.errors = append(parser.errors, err)
setError(sel, err)
return
}
// fmt.Println(sel.Nodes[0].Attr)
sel.SetHtml(html)
parser.parseElementComponent(comsel)
sel.ReplaceWithSelection(comsel)
}
// RenderComponent render the component
func (parser *TemplateParser) RenderComponent(comp *JitComponent, props map[string]interface{}, slots *goquery.Selection, children *goquery.Selection) (string, string, error) {
html := comp.html
func (parser *TemplateParser) newJitComponentSel(sel *goquery.Selection, comp *JitComponent) (*goquery.Selection, error) {
fmt.Println("---", comp.route, "----------------")
fmt.Println(html)
fmt.Println("-------------------------------")
fmt.Println()
html = replaceRandVar(html, Data(props))
fmt.Println("---- after replaceRandVar ----------------")
fmt.Println()
fmt.Println(html)
fmt.Println("-------------------------------")
fmt.Println()
option := *parser.option
option.Route = comp.route
compParser := NewTemplateParser(parser.data, &option)
compParser.sequence = parser.sequence + 1
locale := compParser.Locale()
// Parse the node
ns := Namespace(comp.route, compParser.sequence, comp.buildOption.ScriptMinify)
ns := Namespace(comp.route, parser.sequence+1, comp.buildOption.ScriptMinify)
cn := ComponentName(comp.route, comp.buildOption.ScriptMinify)
sel, err := NewDocumentString(`<body>` + html + `</body>`)
if err != nil {
return "", "", err
props := map[string]string{
"s:ns": ns,
"s:cn": cn,
"s:ready": cn + "()",
}
data := Data{}
for _, attr := range sel.Nodes[0].Attr {
if attr.Key == "is" || attr.Key == "s:jit" {
continue
}
props[attr.Key] = attr.Val
}
root := sel.Find("body")
// JSON props
for key, val := range props {
var v interface{} = val
if props[fmt.Sprintf("json-attr-%s", key)] == "true" {
fmt.Println("json-attr-", key)
err := jsoniter.UnmarshalFromString(val, &v)
if err != nil {
v = fmt.Sprintf(`"%s"`, val)
}
}
data[key] = v
}
doc, err := NewDocumentString(comp.html)
if err != nil {
return nil, fmt.Errorf("Component %s failed to load, please recompile the component. %s", comp.route, err.Error())
}
compSel := doc.Find("body").Children().First()
data.replaceNodeUse(propTokens, compSel.Nodes[0])
for key, val := range props {
if strings.HasPrefix(key, "s:") || strings.HasPrefix(key, "json-attr-") || key == "parsed" {
compSel.SetAttr(key, val)
continue
}
compSel.SetAttr(fmt.Sprintf("prop:%s", key), val)
}
// Replace the slots
slots.Each(func(i int, s *goquery.Selection) {
name := s.AttrOr("name", "")
if name == "" {
return
}
slots := sel.Find("slot")
if slots.Length() > 0 {
for i := 0; i < slots.Length(); i++ {
name := slots.Eq(i).AttrOr("name", "")
if name == "" {
continue
}
// Find the slot
slotSel := root.Find(name)
if slotSel.Length() == 0 {
return
slots.Eq(i).Remove()
slotSel := compSel.Find(name)
if slotSel.Length() == 0 {
continue
}
slotSel.ReplaceWithSelection(slots.Eq(i).Contents())
}
// Replace the slot
slotSel.ReplaceWithSelection(s.Contents())
})
}
// Replace the children
children.Find("slot").Remove()
root.Find("children").ReplaceWithSelection(children)
children := sel.Contents()
compSel.Find("children").ReplaceWithSelection(children)
return compSel, nil
}
func (parser *TemplateParser) getJitComponent(sel *goquery.Selection) (*JitComponent, error) {
is := sel.AttrOr("is", "")
if is == "" {
return nil, fmt.Errorf("Component route is required")
// Replace the props
if locale != nil {
locale.replaceVars(Data(props))
}
compParser.locale = locale
compParser.BindEvent(root, ns, cn)
compParser.RenderSelection(root)
if compParser.scripts != nil {
for _, script := range compParser.scripts {
script.Namespace = ns
parser.scripts = append(parser.scripts, script)
}
if parser.option == nil {
parser.option = &ParserOption{Debug: true, DisableCache: false}
}
if comp.scripts != nil {
for _, script := range comp.scripts {
script.Namespace = ns
parser.scripts = append(parser.scripts, script)
}
// Load the component
if comp, has := Components[is]; has && parser.option.Debug == false && parser.option.DisableCache == false {
return comp, nil
}
if comp.styles != nil {
for _, style := range comp.styles {
style.Namespace = ns
parser.styles = append(parser.styles, style)
}
file := filepath.Join(string(os.PathSeparator), "public", parser.option.Root, is+".jit")
if exist, _ := application.App.Exists(file); !exist {
return nil, fmt.Errorf("Component %s file not found, please recompile the component", is)
}
// Update sequence
parser.sequence = compParser.sequence + parser.sequence
first := root.Children().First()
first.SetAttr("s:ns", ns)
first.SetAttr("s:cn", cn)
first.SetAttr("s:ready", cn+"()")
html, err = root.Html()
source, err := application.App.Read(file)
if err != nil {
return "", "", err
return nil, fmt.Errorf("Component %s failed to load, please recompile the component", is)
}
return html, ns, nil
// Get the scripts
var scriptnodes []ScriptNode = []ScriptNode{}
var stylenodes []StyleNode = []StyleNode{}
var imports map[string]string = map[string]string{}
var buildOption *BuildOption = &BuildOption{}
source, scriptnodes, err = parser.getScriptNodes(source)
if err != nil {
return nil, fmt.Errorf("Component %s failed to load, please recompile the component. %s", is, err.Error())
}
source, stylenodes, err = parser.getStyleNodes(source)
if err != nil {
return nil, fmt.Errorf("Component %s failed to load, please recompile the component. %s", is, err.Error())
}
source, imports, err = parser.getImports(source)
if err != nil {
return nil, fmt.Errorf("Component %s failed to load, please recompile the component. %s", is, err.Error())
}
source, buildOption, err = parser.getBuildOption(source)
if err != nil {
return nil, fmt.Errorf("Component %s failed to load, please recompile the component. %s", is, err.Error())
}
comp := &JitComponent{
file: file,
route: is,
html: string(source),
scripts: scriptnodes,
styles: stylenodes,
imports: imports,
buildOption: buildOption,
}
// Save the component to the cache
chComp <- &componentData{is, comp, saveComponent}
return comp, nil
}
func (parser *TemplateParser) getImports(source []byte) ([]byte, map[string]string, error) {
imports := map[string]string{}
source = reImports.ReplaceAllFunc(source, func(raw []byte) []byte {
raw = reImports.ReplaceAll(raw, []byte("$1"))
err := jsoniter.Unmarshal(raw, &imports)
if err != nil {
return nil
}
return []byte{}
})
return source, imports, nil
}
func (parser *TemplateParser) getBuildOption(source []byte) ([]byte, *BuildOption, error) {
var buildOption BuildOption
rawOption := []byte{}
source = reOption.ReplaceAllFunc(source, func(raw []byte) []byte {
rawOption = reOption.ReplaceAll(raw, []byte("$1"))
return []byte{}
})
if rawOption != nil {
err := jsoniter.Unmarshal(rawOption, &buildOption)
if err != nil {
return source, nil, err
}
}
return source, &buildOption, nil
}
func (parser *TemplateParser) getStyleNodes(source []byte) ([]byte, []StyleNode, error) {
var errs error
nodes := []StyleNode{}
source = reStyles.ReplaceAllFunc(source, func(raw []byte) []byte {
raw = reStyles.ReplaceAll(raw, []byte("$1"))
var stylenodes []StyleNode
err := jsoniter.Unmarshal(raw, &stylenodes)
if err != nil {
errs = multierror.Append(errs, err)
return nil
}
nodes = append(nodes, stylenodes...)
return []byte{}
})
return source, nodes, errs
}
func (parser *TemplateParser) getScriptNodes(source []byte) ([]byte, []ScriptNode, error) {
var errs error
nodes := []ScriptNode{}
// Get the scripts
source = reScripts.ReplaceAllFunc(source, func(raw []byte) []byte {
raw = reScripts.ReplaceAll(raw, []byte("$1"))
var scripts []ScriptNode
err := jsoniter.Unmarshal(raw, &scripts)
if err != nil {
errs = multierror.Append(errs, err)
return nil
}
nodes = append(nodes, scripts...)
return []byte{}
})
return source, nodes, errs
}
func (parser *TemplateParser) filterScripts(parent string, scripts []ScriptNode) []ScriptNode {
@ -201,29 +307,6 @@ func (parser *TemplateParser) addStyles(sel *goquery.Selection, styles []StyleNo
}
}
func (parser *TemplateParser) getComponent(sel *goquery.Selection) (*JitComponent, map[string]interface{}, *goquery.Selection, *goquery.Selection, error) {
slots := sel.Find("slot")
children := sel.Contents()
props, err := parser.componentProps(sel)
if err != nil {
return nil, nil, slots, children, err
}
file, route, err := parser.componentFile(sel, props)
if err != nil {
return nil, props, slots, children, err
}
comp, err := getComponent(route, file, parser.disableCache())
if err != nil {
return nil, props, slots, children, err
}
return comp, props, slots, children, nil
}
// isJitComponent check if the selection is a component
func (parser *TemplateParser) isJitComponent(sel *goquery.Selection) bool {
_, exist := sel.Attr("s:jit")
@ -231,113 +314,6 @@ func (parser *TemplateParser) isJitComponent(sel *goquery.Selection) bool {
return exist && is != ""
}
func (parser *TemplateParser) componentProps(sel *goquery.Selection) (map[string]interface{}, error) {
parentProps := map[string]string{}
props := map[string]string{}
parent := sel.AttrOr("s:parent", "")
parentSel := sel.Parents().Find(fmt.Sprintf(`[s\:ns="%s"]`, parent))
if parentSel != nil && parentSel.Length() > 0 {
for _, attr := range parentSel.Nodes[0].Attr {
if !strings.HasPrefix(attr.Key, "s:prop") {
continue
}
key := strings.TrimPrefix(attr.Key, "s:prop:")
parentProps[key] = attr.Val
}
}
for _, attr := range sel.Nodes[0].Attr {
// s:on , s:data , s:json
if strings.HasPrefix(attr.Key, "s:on") || strings.HasPrefix(attr.Key, "s:data") || strings.HasPrefix(attr.Key, "s:json") {
props[attr.Key] = attr.Val
continue
}
if strings.HasPrefix(attr.Key, "s:") || attr.Key == "is" {
continue
}
if strings.HasPrefix(attr.Key, "...$props") {
data := Data{"$props": parentProps}
values, _, err := data.Exec(fmt.Sprintf("{{ %s }}", strings.TrimPrefix(attr.Key, "...")))
if err != nil {
return map[string]interface{}{}, err
}
if values == nil {
continue
}
if _, ok := values.(map[string]string); ok {
for key, val := range values.(map[string]string) {
props[key] = val
}
}
continue
}
props[attr.Key] = attr.Val
}
return parser.parseComponentProps(props)
}
func (parser *TemplateParser) parseComponentProps(props map[string]string) (map[string]interface{}, error) {
result := map[string]interface{}{}
for key, val := range props {
if strings.HasPrefix(key, "...") {
values, _, err := parser.data.Exec(fmt.Sprintf("{{ %s }}", strings.TrimPrefix(key, "...")))
if err != nil {
return nil, err
}
if values == nil {
continue
}
if _, ok := values.(map[string]interface{}); ok {
for k := range values.(map[string]interface{}) {
result[k] = k
}
}
continue
}
result[key] = val
}
return result, nil
}
func (parser *TemplateParser) componentFile(sel *goquery.Selection, props map[string]interface{}) (string, string, error) {
route := sel.AttrOr("is", "")
if route == "" {
return "", "", fmt.Errorf("Component route is required")
}
data := Data{"$props": props}
route, _ = data.ReplaceUse(dataTokens, route)
route, _ = parser.data.Replace(route)
file := filepath.Join(string(os.PathSeparator), "public", parser.option.Root, route+".jit")
return file, route, nil
}
func (locale *Locale) replaceVars(data Data) {
if locale.Keys != nil {
for key, val := range locale.Keys {
locale.Keys[key] = replaceRandVar(val, data)
}
}
if locale.Messages != nil {
for key, val := range locale.Messages {
locale.Messages[key] = replaceRandVar(val, data)
}
}
}
func componentWriter() {
for {
select {
@ -351,110 +327,3 @@ func componentWriter() {
}
}
}
func getComponent(route string, file string, disableCache ...bool) (*JitComponent, error) {
noCache := false
if len(disableCache) > 0 && disableCache[0] {
noCache = true
}
if comp, ok := Components[file]; ok && !noCache {
return comp, nil
}
comp, err := readComponent(route, file)
if err != nil {
return nil, err
}
// Save the component to the cache
chComp <- &componentData{file, comp, saveComponent}
return comp, nil
}
// readComponent read the JIT component
func readComponent(route string, file string) (*JitComponent, error) {
content, err := application.App.Read(file)
if err != nil {
return nil, err
}
// Get the scripts
html := string(content)
rawScripts := ""
rawStyles := ""
rawOption := ""
html = reScripts.ReplaceAllStringFunc(html, func(exp string) string {
rawScripts = reScripts.ReplaceAllString(exp, "$1")
return ""
})
html = reStyles.ReplaceAllStringFunc(html, func(exp string) string {
rawStyles = reStyles.ReplaceAllString(exp, "$1")
return ""
})
html = reOption.ReplaceAllStringFunc(html, func(exp string) string {
rawOption = reOption.ReplaceAllString(exp, "$1")
return ""
})
scripts := []ScriptNode{}
styles := []StyleNode{}
buildOption := BuildOption{}
if rawScripts != "" {
err := jsoniter.UnmarshalFromString(rawScripts, &scripts)
if err != nil {
return nil, err
}
}
if rawStyles != "" {
err := jsoniter.UnmarshalFromString(rawStyles, &styles)
if err != nil {
return nil, err
}
}
if rawOption != "" {
err := jsoniter.UnmarshalFromString(rawOption, &buildOption)
if err != nil {
return nil, err
}
}
return &JitComponent{
file: file,
route: route,
html: html,
scripts: scripts,
styles: styles,
buildOption: &buildOption,
}, nil
}
func replaceRandVar(value string, data Data) string {
return propTokens.ReplaceAllStringFunc(value, func(exp string) string {
if strings.HasPrefix(exp, "[{") && strings.HasSuffix(exp, "}]") {
matches := propTokens.FindAllStringSubmatch(exp, -1)
if len(matches) > 0 {
identifiers, err := data.Identifiers(matches[0][1])
if err != nil {
return err.Error()
}
for _, identifier := range identifiers {
exp = strings.ReplaceAll(exp, identifier.Value, strings.ReplaceAll(identifier.Value, "$props.", ""))
}
}
}
res := data.ExecString(exp)
if res.Error != nil {
return res.Error.Error()
}
return res.Value
})
}

View file

@ -256,16 +256,17 @@ func (parser *TemplateParser) parseElementNode(sel *goquery.Selection) {
func (parser *TemplateParser) parseElementComponent(sel *goquery.Selection) {
sel.SetAttr("parsed", "true")
parser.parsed(sel)
com := sel.AttrOr("s:cn", "")
props := map[string]interface{}{}
for _, attr := range sel.Nodes[0].Attr {
if !strings.HasPrefix(attr.Key, "s:") && attr.Key != "parsed" {
if strings.HasPrefix(attr.Key, "prop:") {
var val any = attr.Val
var key = strings.TrimPrefix(attr.Key, "prop:")
if _, exist := sel.Attr("json-attr-" + attr.Key); exist {
val = ValueJSON(attr.Val)
}
props[attr.Key] = val
props[key] = val
}
}
@ -299,6 +300,11 @@ func (parser *TemplateParser) parseElementComponent(sel *goquery.Selection) {
compParser.data[k] = v
}
}
raw, err := jsoniter.MarshalToString(data)
if err != nil {
raw = fmt.Sprintf(`"%s"`, err.Error())
}
sel.SetAttr("json:__component_data", raw)
}
err = compParser.RenderSelection(sel)
@ -307,6 +313,7 @@ func (parser *TemplateParser) parseElementComponent(sel *goquery.Selection) {
setError(sel, err)
}
parser.sequence = compParser.sequence + 1
}
func (parser *TemplateParser) clone(script *Script) *TemplateParser {

View file

@ -126,7 +126,11 @@ func (r *Request) ExecString(data string) (Data, error) {
if err != nil {
return nil, err
}
r.Exec(res)
err = r.Exec(res)
if err != nil {
return nil, err
}
return res, nil
}
@ -288,7 +292,11 @@ func (r *Request) call(p interface{}) (interface{}, error) {
// Call the backend script
if r.Script != nil && strings.HasPrefix(processName, "@") {
method := processName[1:]
return r.Script.Call(r, method, processArgs...)
v, err := r.Script.Call(r, method, processArgs...)
if err != nil {
return nil, fmt.Errorf("backend script %s %s, please check the script", method, err.Error())
}
return v, nil
}
process, err := process.Of(processName, processArgs...)

View file

@ -54,11 +54,11 @@ func scriptWriter() {
// LoadScript load the script
func LoadScript(file string) (*Script, error) {
if script, has := Scripts[file]; has {
base := strings.TrimSuffix(strings.TrimSuffix(file, ".sui"), ".jit")
if script, has := Scripts[base]; has {
return script, nil
}
base := strings.TrimSuffix(file, ".sui")
file = base + ".ts"
if exist, _ := application.App.Exists(file); !exist {
file = base + ".js"
@ -79,7 +79,7 @@ func LoadScript(file string) (*Script, error) {
}
script := &Script{Script: v8script}
chScript <- &scriptData{file, script, saveScript}
chScript <- &scriptData{base, script, saveScript}
return script, nil
}