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 + ':' portreturn (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 3986function 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文件中的exportswindow.exports = {};window.module={exports:{}};window.eval(code);//console.log(window.exports);//console.log(window.module);let obj;if (window.exports.default)obj=window.exports.default;elseobj=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文件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);//不能切换到新的tab,应该是个buglet 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属性,都没搞定。