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:{}};
window.eval(code);
//console.log(window.exports);
//console.log(window.module);
let obj;
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文件
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,应该是个bug
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属性,都没搞定。