建立无需build的react单页面应用SPA框架(1)

vue、react这种前端渲染的框架,比较适合做SPA。如果用ejs做SPA(Single Page Application),js代码控制好全局变量冲突不算严重,但dom元素用jquery操作会遇到很多的名称上的冲突(tag、id、name)。

SPA要解决的问题:

(1)业务组件用什么文件格式?如果使用*.jsx文件,需要在部署前build转换。本来js的初心就是“即改即用”,我不太喜欢ts,jsx这些需要build的东西,前端加一个babel来转换。

(2)业务组件如何加载?业务组件不可能写的时候全部知道(根据用户权限决定),也不可能一次性全部加载(影响首屏效率),应该是需要的时候,才从服务器加载。加载的jsx文件经过babel转换成js后,用eval函数执行。


demo.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Acro Multi-Lang Demo</title>
    <script src="/js/jquery-1.11.1/jquery-1.11.1.min.js"></script>
    <script src="/src/acroMulti.Resources.js"></script>
    <!-- <script src="/src/acroMulti.HTML.TagMethod.js"></script>
    <script src="/src/acroMulti.HTML.TagMethod.Register.js"></script>
    <script src="/src/acroMulti.HTML.Replacer.js"></script> -->
    <script src="/src/acroMulti.DD.js"></script>
    <script src="/src/acroMulti.CSVText.js"></script>
    <script src="/src/acroMulti.DD.CSVText.js"></script>
    <!-- <script src="/src/acroMulti.Locale.js"></script> -->
    <script src="/src/acroMulti.Culture.js"></script>
    <script src="/src/acroMulti.Utils.js"></script>
    <script src="/dd/dd.unicode.lng.base64.js"></script>
    <script src="/src/acroMulti.Browser.Engine.js"></script>
    <script src="/src/acroMulti.Tool.Chinese.js"></script>
    <!-- <link rel="stylesheet" type="text/css" href="/jsx/src/css.main.css"/> -->
    <!-- <link rel="stylesheet" type="text/css" href='/js/rc-easyui-1.2.9/dist/themes/default/easyui.css'>
    <link rel="stylesheet" type="text/css" href='/js/rc-easyui-1.2.9/themes/icon.css'>
    <link rel="stylesheet" type="text/css" href='/js/rc-easyui-1.2.9/themes/react.css'> -->
    <script type="importmap">
      {
        "imports": {
          "react": "/js/react-18.1.0/react.development.js",
          "easyui":"/js/rc-easyui-1.2.9/dist/rc-easyui-min.js"
        }
      }
    </script>
    <style>
      @import '/js/rc-easyui-1.2.9/dist/themes/default/easyui.css';
      @import '/js/rc-easyui-1.2.9/dist/themes/icon.css';
      @import '/js/rc-easyui-1.2.9/dist/themes/react.css';
    </style>
  </head>
  <body>
    <div>
      <img src="/img/AcroMultiLanguage4.1.gif"/>
    </div>
    <div id="div_main"></div>
    <script src="/js/react-18.1.0/react.development.js"></script>
    <script src="/js/react-18.1.0/react-dom.development.js"></script>
    <script src="/js/babel-7.17.11/babel.min.js"></script>

    <script>
      let importMap=$('script[type="importmap"]').text();
      //console.log(importMap);
      importMap=JSON.parse(importMap).imports;
      function parseURI(url) {
        var m = String(url).replace(/^\s+|\s+$/g, '').match(/^([^:\/?#]+:)?(\/\/(?:[^:@]*(?::[^:@]*)?@)?(([^:\/?#]*)(?::(\d*))?))?([^?#]*)(\?[^#]*)?(#[\s\S]*)?/);
        // authority = '//' + user + ':' + pass '@' + hostname + ':' port
        return (m ? {
          href     : m[0] || '',
          protocol : m[1] || '',
          authority: m[2] || '',
          host     : m[3] || '',
          hostname : m[4] || '',
          port     : m[5] || '',
          pathname : m[6] || '',
          search   : m[7] || '',
          hash     : m[8] || ''
        } : null);
      }
      
      function absolutizeURI(base, href) {// RFC 3986
        function removeDotSegments(input) {
          var output = [];
          input.replace(/^(\.\.?(\/|$))+/, '')
              .replace(/\/(\.(\/|$))+/g, '/')
              .replace(/\/\.\.$/, '/../')
              .replace(/\/?[^\/]*/g, function (p) {
            if (p === '/..') {
              output.pop();
            } else {
              output.push(p);
            }
          });
          return output.join('').replace(/^\//, input.charAt(0) === '/' ? '/' : '');
        }
      
        href = parseURI(href || '');
        base = parseURI(base || '');
      
        return !href || !base ? null : (href.protocol || base.protocol) +
              (href.protocol || href.authority ? href.authority : base.authority) +
              removeDotSegments(href.protocol || href.authority || href.pathname.charAt(0) === '/' ? href.pathname : (href.pathname ? ((base.authority && !base.pathname ? '/' : '') + base.pathname.slice(0, base.pathname.lastIndexOf('/') + 1) + href.pathname) : base.pathname)) +
              (href.protocol || href.authority || href.pathname ? href.search : (href.search || base.search)) +
              href.hash;
      }

      function invokeCode(file,rawCode){
        // console.log(file);
        // if (invokeCode.caller) console.log(invokeCode.caller.arguments);
        let code=rawCode;
        if (file.substr(file.length-4).toLowerCase()=='.jsx'){
          code = Babel.transform(code,{presets: ['es2015','react']}).code;
          //console.log(code); 
        }
        //用hook模式支持jsx文件中的exports
        window.exports = {};
        window.module={exports:{}};
        let obj=window.eval(code);
        //console.log(window.exports);
        //console.log(window.module);
        if (obj===true){
          if (window.exports.default)
            obj=window.exports.default;
          else
            obj=window.module.exports;
        }
        //let obj=g_eval(code);//全局作用域
        //let obj=eval.call(this,code);
        //let obj=g_eval('('+ code + ')');
        //let obj=window.Function('"use strict";return (' + code + ')')();
        // console.log('code3:',module);
        // console.log(obj);
        return obj;
      }
      //babel.min.js处理import指令需要require函数
      //js的import函数不能加载jsx文件\
      //或者用https://www.npmjs.com/package/breq这个改造一下
      window.require=function(file){
        //console.log('1.raw:',file);
        if (importMap[file]){
          file=importMap[file];
        }
        //处理相对路径
        let root;
        if (require.caller==invokeCode){
          root=require.caller.arguments[0];
        }
        else{
          root=window.location.pathname;
        }
        //console.log('2.root:',root);
        file=absolutizeURI(root,file);
        //console.log('3.absolute:',file);

        let xhr = new XMLHttpRequest();
        xhr.open("GET", file, false);
        xhr.send();
        if(xhr.status != 200) {
          throw new Error(file+",require error: http status " + xhr.status);
        }
        let code=xhr.responseText;
        //console.log(code);
        return invokeCode(file,code);
      }
      /*
      //require要求同步函数,fetch是异步函数无法使用
      window.require=async function(module){
        console.log(module);
        let res=await fetch(module);
        console.log(res);
        let code=await res.text();
        console.log(code);
        return invokeCode(module,code);
      }
      */
    </script>
    <script type="text/babel">
      import Com_Main from './com.main.jsx';
      let root_main,el_main,div_main;
      function render_main(){
        if (!root_main){
          div_main =$('#div_main')[0];
          root_main = ReactDOM.createRoot(div_main);
        }
        el_main=React.createElement(Com_Main);
        root_main.render(el_main);
      }

      acroMulti.engine.switchLanguage=function(){
        render_main();
        // acroMulti.engine.replaceElements($('title'));
      }
      acroMulti.engine.switchLanguage();
    </script>
  </body>
</html>

babel需要require函数,浏览器没有这个函数,必须是同步函数,浏览器原生fetch函数是异步的不可用。我们自己写一个require函数来加载jsx业务组件文件。用了函数的caller来处理相对路径问题。用了importmap来处理组件加载名称问题。

页面划分为上中下三层,中间划分为左右两部分,左边是功能树,右边是功能区。

com.main.jsx

import Com_Header from './com.header.jsx';
import Com_Left from './com.left.jsx';
import Com_Right from './com.right.jsx';
import Com_Language_Engine from './com.language.engine.jsx';
import {Resizable} from 'easyui';
let t=acroMulti.t;
class Com_Main extends React.Component {
  constructor(props){
    super(props);
    this.switchTab=this.switchTab.bind(this);
    this.ref_right = React.createRef(null);
  }
  switchTab(name,file){
    this.ref_right.current.switchTab(name,file);
  }
  render() {
    return (
        <div>
            <a href="/">{t('Home')}</a>
            <h1>{t('Demo:translate at frontend browser,translate needed(React+jsx)')}</h1>
            <span>SPA:Single Page Application</span>
            <div className='layout-header' style={{backgroundColor:'bisque'}}>
                <Com_Header></Com_Header>
            </div>
            <div className='layout-middle'>
                <Resizable minWidth='200' handles='e'>
                  <div className='layout-left' style={{width:'200px',float:'left',overflow: 'hidden',backgroundColor:'aquamarine'}}>
                      <Com_Left switchTab={this.switchTab}></Com_Left>
                  </div>
                </Resizable>
                <div className='layout-right' style={{marginLeft:'200px',overflow: 'hidden'}}>
                  <Com_Right ref={this.ref_right}></Com_Right> 
                </div>
                <div style={{clear:'both'}}></div>
            </div>
            <div className='layout-footer' style={{backgroundColor:'brown',textAlign:'center'}}>
                <span>copyright© Acroprise Inc. 2001-2023</span>
            </div>
            <Com_Language_Engine></Com_Language_Engine>
      </div>
    );
  }
}
export default Com_Main;

com.left.jsx

class Com_Left extends React.Component {
  constructor(props) {
    super(props);
    //this.state = {};
    this.menu_click = this.menu_click.bind(this);
  }
  menu_click(e){
    //console.log(e);
    e.preventDefault();
    //root_right.render();
    let name=e.target.innerHTML;
    let file=e.target.getAttribute('file');
    this.props.switchTab(name,file);
  }
  render() {
    console.log('render left');
    return (
      <div>
        <a href='/'>{acroMulti.t('Home')}</a><br/>
        <a href='/DDEditor' onClick={this.menu_click} file='/react/app/DDEditor/page.ddeditor.jsx'>{acroMulti.t('Data Dictionary Editor')}</a><br/>
        <a href='/likeButton' onClick={this.menu_click} file='/react/app/likeButton/page.likeButton.jsx'>{acroMulti.t('Like Button')}</a><br/>
        <a href='/About' onClick={this.menu_click} file=''>{acroMulti.t('&About')}</a>
      </div>
    );
  }
}
export default Com_Left;

com.right.jsx

import {Tabs,TabPanel} from 'easyui';
import Com_bizCom from './com.bizCom.jsx';

class Com_Right extends React.Component {
    constructor(props){
      console.log('Com_Right constructor');
        super(props);
        this.state={
          tabs:[],
          tabIndex:0,
          tabSelected:''
        }
        this.ref_tabs=React.createRef(null);
        this.ref_tabItems=React.createRef(null);
        this.onTabClose=this.onTabClose.bind(this);
        this.onTabSelect=this.onTabSelect.bind(this);
    }
    switchTab(name,file){
        console.log(name,file);
        console.log(this.state.tabs);
        console.log(this.ref_tabs.current);
        //this.setState({file:file});
        //this.state.file=file;
        let tab=null;
        for(let i=0;i<this.state.tabs.length;i++){
          if (this.state.tabs[i].name==name){
            tab=this.state.tabs[i];
            this.ref_tabs.current.select(i);
            break;
          }
        }
        if (!tab){
          this.state.tabs.push({name,file});
          this.state.tabIndex=this.state.tabs.length-1;
          this.state.tabSelected=name;
          this.setState(this.state,function(){
            tabs不能切换到新的tab,应该是个bug,改用panel
            //self.ref_tabs.current.select(self.state.tabs.length-1);
            let panel=this.ref_tabs.current.panels[this.ref_tabs.current.panels.length-1];
            panel.select();
          });
          
          let self=this;
          //self.ref_tabs.current.replaceProps({selctedIndex:self.state.tabs.length-1})
          // this.forceUpdate(function(){
          //   self.ref_tabs.current.select(self.state.tabs.length-1);
          // });
        
          //my god,只有延迟1秒有效
          // setTimeout(function(){
          //   self.ref_tabs.current.select(self.state.tabs.length-1);
          // }, 1000);
        }
        //this.forceUpdate();
        //this.ref_tabs.current.forceUpdate();
        //this.ref_right.current.setState({file:file});
        //this.ref_right.current.forceUpdate();
    }
    onTabSelect(tab){
        console.log('onTabSelect',tab);
        console.log(this.ref_tabs.current);
        for(let i=0;i<this.state.tabs.length;i++){
          if (this.state.tabs[i].name==tab.props.title){
            this.state.tabIndex=i;
            this.state.tabSelected=tab.props.title;
            break;
          }
        }
    }
    onTabClose(tab){
      console.log(tab);
      for(let i=0;i<this.state.tabs.length;i++){
        if (this.state.tabs[i].name==tab.props.title){
          this.state.tabs.splice(i,1);
          console.log(this.state.tabs);
          this.setState(this.state);
          break;
        }
      }
    }
    componentDidUpdate(e){
        //不起作用
        console.log('componentDidUpdate',e,this.state.tabIndex);
        //this.ref_tabs.current.select(this.state.tabIndex);
    }
    
    render(){
      let self=this;
      let tabs=this.state.tabs.map(function(tab){
          return (
            <TabPanel title={tab.name} closable='true' key={tab.name} selected={self.state.tabSelected==tab.name}>
              <Com_bizCom file={tab.file}></Com_bizCom>
            </TabPanel>
          )
      });
        
      return(
          <Tabs ref={this.ref_tabs} onTabSelect={this.onTabSelect}
            
            plain='true' scrollable="true" onTabClose={this.onTabClose}>
            {tabs}
          </Tabs>
      );
    }
}

export default Com_Right;

com.bizCom.jsx

class Com_bizCom extends React.Component {
  constructor(props) {
    super(props);
  }
  shouldComponentUpdate(nextProps, nextState) {
    //console.log(nextProps);
    //文件相同时不要再渲染
    if (nextProps.file && (nextProps.file === this.props.file)) return false;
    return true;
  }
  render() {
    //console.log('Com_bizCom',this.props);
    if (!this.props.file) return null;
    /*
    //import函数不能加载jsx文件
    import(this.state.file).then(function(res){
      console.log(res);
    });
    return;
    */
    let Obj=window.require(this.props.file);
    //console.log(Obj);
    let com=React.createElement(Obj);
    return com;
  }
}
export default Com_bizCom;

效果如下图:

react版本的easyui的tabs元件,可能有bug,新增加的tabPanel不会被选中,无论用tabs的select函数,还是用tabs的selectedIndex属性,或者tabPanel的selected属性,都没搞定。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

火星牛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值