当前位置:   article > 正文

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

spa框架

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

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8" />
  5. <title>Acro Multi-Lang Demo</title>
  6. <script src="/js/jquery-1.11.1/jquery-1.11.1.min.js"></script>
  7. <script src="/src/acroMulti.Resources.js"></script>
  8. <!-- <script src="/src/acroMulti.HTML.TagMethod.js"></script>
  9. <script src="/src/acroMulti.HTML.TagMethod.Register.js"></script>
  10. <script src="/src/acroMulti.HTML.Replacer.js"></script> -->
  11. <script src="/src/acroMulti.DD.js"></script>
  12. <script src="/src/acroMulti.CSVText.js"></script>
  13. <script src="/src/acroMulti.DD.CSVText.js"></script>
  14. <!-- <script src="/src/acroMulti.Locale.js"></script> -->
  15. <script src="/src/acroMulti.Culture.js"></script>
  16. <script src="/src/acroMulti.Utils.js"></script>
  17. <script src="/dd/dd.unicode.lng.base64.js"></script>
  18. <script src="/src/acroMulti.Browser.Engine.js"></script>
  19. <script src="/src/acroMulti.Tool.Chinese.js"></script>
  20. <!-- <link rel="stylesheet" type="text/css" href="/jsx/src/css.main.css"/> -->
  21. <!-- <link rel="stylesheet" type="text/css" href='/js/rc-easyui-1.2.9/dist/themes/default/easyui.css'>
  22. <link rel="stylesheet" type="text/css" href='/js/rc-easyui-1.2.9/themes/icon.css'>
  23. <link rel="stylesheet" type="text/css" href='/js/rc-easyui-1.2.9/themes/react.css'> -->
  24. <script type="importmap">
  25. {
  26. "imports": {
  27. "react": "/js/react-18.1.0/react.development.js",
  28. "easyui":"/js/rc-easyui-1.2.9/dist/rc-easyui-min.js"
  29. }
  30. }
  31. </script>
  32. <style>
  33. @import '/js/rc-easyui-1.2.9/dist/themes/default/easyui.css';
  34. @import '/js/rc-easyui-1.2.9/dist/themes/icon.css';
  35. @import '/js/rc-easyui-1.2.9/dist/themes/react.css';
  36. </style>
  37. </head>
  38. <body>
  39. <div>
  40. <img src="/img/AcroMultiLanguage4.1.gif"/>
  41. </div>
  42. <div id="div_main"></div>
  43. <script src="/js/react-18.1.0/react.development.js"></script>
  44. <script src="/js/react-18.1.0/react-dom.development.js"></script>
  45. <script src="/js/babel-7.17.11/babel.min.js"></script>
  46. <script>
  47. let importMap=$('script[type="importmap"]').text();
  48. //console.log(importMap);
  49. importMap=JSON.parse(importMap).imports;
  50. function parseURI(url) {
  51. var m = String(url).replace(/^\s+|\s+$/g, '').match(/^([^:\/?#]+:)?(\/\/(?:[^:@]*(?::[^:@]*)?@)?(([^:\/?#]*)(?::(\d*))?))?([^?#]*)(\?[^#]*)?(#[\s\S]*)?/);
  52. // authority = '//' + user + ':' + pass '@' + hostname + ':' port
  53. return (m ? {
  54. href : m[0] || '',
  55. protocol : m[1] || '',
  56. authority: m[2] || '',
  57. host : m[3] || '',
  58. hostname : m[4] || '',
  59. port : m[5] || '',
  60. pathname : m[6] || '',
  61. search : m[7] || '',
  62. hash : m[8] || ''
  63. } : null);
  64. }
  65. function absolutizeURI(base, href) {// RFC 3986
  66. function removeDotSegments(input) {
  67. var output = [];
  68. input.replace(/^(\.\.?(\/|$))+/, '')
  69. .replace(/\/(\.(\/|$))+/g, '/')
  70. .replace(/\/\.\.$/, '/../')
  71. .replace(/\/?[^\/]*/g, function (p) {
  72. if (p === '/..') {
  73. output.pop();
  74. } else {
  75. output.push(p);
  76. }
  77. });
  78. return output.join('').replace(/^\//, input.charAt(0) === '/' ? '/' : '');
  79. }
  80. href = parseURI(href || '');
  81. base = parseURI(base || '');
  82. return !href || !base ? null : (href.protocol || base.protocol) +
  83. (href.protocol || href.authority ? href.authority : base.authority) +
  84. 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)) +
  85. (href.protocol || href.authority || href.pathname ? href.search : (href.search || base.search)) +
  86. href.hash;
  87. }
  88. function invokeCode(file,rawCode){
  89. // console.log(file);
  90. // if (invokeCode.caller) console.log(invokeCode.caller.arguments);
  91. let code=rawCode;
  92. if (file.substr(file.length-4).toLowerCase()=='.jsx'){
  93. code = Babel.transform(code,{presets: ['es2015','react']}).code;
  94. //console.log(code);
  95. }
  96. //用hook模式支持jsx文件中的exports
  97. window.exports = {};
  98. window.module={exports:{}};
  99. let obj=window.eval(code);
  100. //console.log(window.exports);
  101. //console.log(window.module);
  102. if (obj===true){
  103. if (window.exports.default)
  104. obj=window.exports.default;
  105. else
  106. obj=window.module.exports;
  107. }
  108. //let obj=g_eval(code);//全局作用域
  109. //let obj=eval.call(this,code);
  110. //let obj=g_eval('('+ code + ')');
  111. //let obj=window.Function('"use strict";return (' + code + ')')();
  112. // console.log('code3:',module);
  113. // console.log(obj);
  114. return obj;
  115. }
  116. //babel.min.js处理import指令需要require函数
  117. //js的import函数不能加载jsx文件\
  118. //或者用https://www.npmjs.com/package/breq这个改造一下
  119. window.require=function(file){
  120. //console.log('1.raw:',file);
  121. if (importMap[file]){
  122. file=importMap[file];
  123. }
  124. //处理相对路径
  125. let root;
  126. if (require.caller==invokeCode){
  127. root=require.caller.arguments[0];
  128. }
  129. else{
  130. root=window.location.pathname;
  131. }
  132. //console.log('2.root:',root);
  133. file=absolutizeURI(root,file);
  134. //console.log('3.absolute:',file);
  135. let xhr = new XMLHttpRequest();
  136. xhr.open("GET", file, false);
  137. xhr.send();
  138. if(xhr.status != 200) {
  139. throw new Error(file+",require error: http status " + xhr.status);
  140. }
  141. let code=xhr.responseText;
  142. //console.log(code);
  143. return invokeCode(file,code);
  144. }
  145. /*
  146. //require要求同步函数,fetch是异步函数无法使用
  147. window.require=async function(module){
  148. console.log(module);
  149. let res=await fetch(module);
  150. console.log(res);
  151. let code=await res.text();
  152. console.log(code);
  153. return invokeCode(module,code);
  154. }
  155. */
  156. </script>
  157. <script type="text/babel">
  158. import Com_Main from './com.main.jsx';
  159. let root_main,el_main,div_main;
  160. function render_main(){
  161. if (!root_main){
  162. div_main =$('#div_main')[0];
  163. root_main = ReactDOM.createRoot(div_main);
  164. }
  165. el_main=React.createElement(Com_Main);
  166. root_main.render(el_main);
  167. }
  168. acroMulti.engine.switchLanguage=function(){
  169. render_main();
  170. // acroMulti.engine.replaceElements($('title'));
  171. }
  172. acroMulti.engine.switchLanguage();
  173. </script>
  174. </body>
  175. </html>

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

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

com.main.jsx

  1. import Com_Header from './com.header.jsx';
  2. import Com_Left from './com.left.jsx';
  3. import Com_Right from './com.right.jsx';
  4. import Com_Language_Engine from './com.language.engine.jsx';
  5. import {Resizable} from 'easyui';
  6. let t=acroMulti.t;
  7. class Com_Main extends React.Component {
  8. constructor(props){
  9. super(props);
  10. this.switchTab=this.switchTab.bind(this);
  11. this.ref_right = React.createRef(null);
  12. }
  13. switchTab(name,file){
  14. this.ref_right.current.switchTab(name,file);
  15. }
  16. render() {
  17. return (
  18. <div>
  19. <a href="/">{t('Home')}</a>
  20. <h1>{t('Demo:translate at frontend browser,translate needed(React+jsx)')}</h1>
  21. <span>SPA:Single Page Application</span>
  22. <div className='layout-header' style={{backgroundColor:'bisque'}}>
  23. <Com_Header></Com_Header>
  24. </div>
  25. <div className='layout-middle'>
  26. <Resizable minWidth='200' handles='e'>
  27. <div className='layout-left' style={{width:'200px',float:'left',overflow: 'hidden',backgroundColor:'aquamarine'}}>
  28. <Com_Left switchTab={this.switchTab}></Com_Left>
  29. </div>
  30. </Resizable>
  31. <div className='layout-right' style={{marginLeft:'200px',overflow: 'hidden'}}>
  32. <Com_Right ref={this.ref_right}></Com_Right>
  33. </div>
  34. <div style={{clear:'both'}}></div>
  35. </div>
  36. <div className='layout-footer' style={{backgroundColor:'brown',textAlign:'center'}}>
  37. <span>copyright© Acroprise Inc. 2001-2023</span>
  38. </div>
  39. <Com_Language_Engine></Com_Language_Engine>
  40. </div>
  41. );
  42. }
  43. }
  44. export default Com_Main;

com.left.jsx

  1. class Com_Left extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. //this.state = {};
  5. this.menu_click = this.menu_click.bind(this);
  6. }
  7. menu_click(e){
  8. //console.log(e);
  9. e.preventDefault();
  10. //root_right.render();
  11. let name=e.target.innerHTML;
  12. let file=e.target.getAttribute('file');
  13. this.props.switchTab(name,file);
  14. }
  15. render() {
  16. console.log('render left');
  17. return (
  18. <div>
  19. <a href='/'>{acroMulti.t('Home')}</a><br/>
  20. <a href='/DDEditor' onClick={this.menu_click} file='/react/app/DDEditor/page.ddeditor.jsx'>{acroMulti.t('Data Dictionary Editor')}</a><br/>
  21. <a href='/likeButton' onClick={this.menu_click} file='/react/app/likeButton/page.likeButton.jsx'>{acroMulti.t('Like Button')}</a><br/>
  22. <a href='/About' onClick={this.menu_click} file=''>{acroMulti.t('&About')}</a>
  23. </div>
  24. );
  25. }
  26. }
  27. export default Com_Left;

com.right.jsx

  1. import {Tabs,TabPanel} from 'easyui';
  2. import Com_bizCom from './com.bizCom.jsx';
  3. class Com_Right extends React.Component {
  4. constructor(props){
  5. console.log('Com_Right constructor');
  6. super(props);
  7. this.state={
  8. tabs:[],
  9. tabIndex:0,
  10. tabSelected:''
  11. }
  12. this.ref_tabs=React.createRef(null);
  13. this.ref_tabItems=React.createRef(null);
  14. this.onTabClose=this.onTabClose.bind(this);
  15. this.onTabSelect=this.onTabSelect.bind(this);
  16. }
  17. switchTab(name,file){
  18. console.log(name,file);
  19. console.log(this.state.tabs);
  20. console.log(this.ref_tabs.current);
  21. //this.setState({file:file});
  22. //this.state.file=file;
  23. let tab=null;
  24. for(let i=0;i<this.state.tabs.length;i++){
  25. if (this.state.tabs[i].name==name){
  26. tab=this.state.tabs[i];
  27. this.ref_tabs.current.select(i);
  28. break;
  29. }
  30. }
  31. if (!tab){
  32. this.state.tabs.push({name,file});
  33. this.state.tabIndex=this.state.tabs.length-1;
  34. this.state.tabSelected=name;
  35. this.setState(this.state,function(){
  36. tabs不能切换到新的tab,应该是个bug,改用panel
  37. //self.ref_tabs.current.select(self.state.tabs.length-1);
  38. let panel=this.ref_tabs.current.panels[this.ref_tabs.current.panels.length-1];
  39. panel.select();
  40. });
  41. let self=this;
  42. //self.ref_tabs.current.replaceProps({selctedIndex:self.state.tabs.length-1})
  43. // this.forceUpdate(function(){
  44. // self.ref_tabs.current.select(self.state.tabs.length-1);
  45. // });
  46. //my god,只有延迟1秒有效
  47. // setTimeout(function(){
  48. // self.ref_tabs.current.select(self.state.tabs.length-1);
  49. // }, 1000);
  50. }
  51. //this.forceUpdate();
  52. //this.ref_tabs.current.forceUpdate();
  53. //this.ref_right.current.setState({file:file});
  54. //this.ref_right.current.forceUpdate();
  55. }
  56. onTabSelect(tab){
  57. console.log('onTabSelect',tab);
  58. console.log(this.ref_tabs.current);
  59. for(let i=0;i<this.state.tabs.length;i++){
  60. if (this.state.tabs[i].name==tab.props.title){
  61. this.state.tabIndex=i;
  62. this.state.tabSelected=tab.props.title;
  63. break;
  64. }
  65. }
  66. }
  67. onTabClose(tab){
  68. console.log(tab);
  69. for(let i=0;i<this.state.tabs.length;i++){
  70. if (this.state.tabs[i].name==tab.props.title){
  71. this.state.tabs.splice(i,1);
  72. console.log(this.state.tabs);
  73. this.setState(this.state);
  74. break;
  75. }
  76. }
  77. }
  78. componentDidUpdate(e){
  79. //不起作用
  80. console.log('componentDidUpdate',e,this.state.tabIndex);
  81. //this.ref_tabs.current.select(this.state.tabIndex);
  82. }
  83. render(){
  84. let self=this;
  85. let tabs=this.state.tabs.map(function(tab){
  86. return (
  87. <TabPanel title={tab.name} closable='true' key={tab.name} selected={self.state.tabSelected==tab.name}>
  88. <Com_bizCom file={tab.file}></Com_bizCom>
  89. </TabPanel>
  90. )
  91. });
  92. return(
  93. <Tabs ref={this.ref_tabs} onTabSelect={this.onTabSelect}
  94. plain='true' scrollable="true" onTabClose={this.onTabClose}>
  95. {tabs}
  96. </Tabs>
  97. );
  98. }
  99. }
  100. export default Com_Right;

com.bizCom.jsx

  1. class Com_bizCom extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. }
  5. shouldComponentUpdate(nextProps, nextState) {
  6. //console.log(nextProps);
  7. //文件相同时不要再渲染
  8. if (nextProps.file && (nextProps.file === this.props.file)) return false;
  9. return true;
  10. }
  11. render() {
  12. //console.log('Com_bizCom',this.props);
  13. if (!this.props.file) return null;
  14. /*
  15. //import函数不能加载jsx文件
  16. import(this.state.file).then(function(res){
  17. console.log(res);
  18. });
  19. return;
  20. */
  21. let Obj=window.require(this.props.file);
  22. //console.log(Obj);
  23. let com=React.createElement(Obj);
  24. return com;
  25. }
  26. }
  27. export default Com_bizCom;

效果如下图:

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/1016258
推荐阅读
相关标签
  

闽ICP备14008679号