浏览器require加载devextreme-react组件

十几年前使用了devexpress公司的delphi元件,功能很强。它们的html元件devextreme,功能表现类似,也行强。

devextreme和devextreme-react,我使用的是23.2.3版本。

官方推荐的用法,都是要经过build:

npx devextreme-cli new react-app app-name
cd app-name
npm run start

经过约12秒的build,出来的html中有一个bundle.js,大小为12.5M。

它的模式是MPA,用babel把react组件转换成了js代码。

如果我要做SPA,不要每次都build,如何做?

devextreme和devextrem-react提供了cjs和esm两套代码,但是react本身没有esm的代码版本,如果devextreme使用esm版本,还是要经过babel转成es5来使用。

我们用es5的cjs版本吧。

(1)首先要在浏览器实现一个require函数,我是用breq - npm来改的。

(2)后台如何提供模组加载,我用nodejs实现静态文件。

会遇到几个问题:

(1)引用的组件不是相对路径,不是.、..、/打头的。

如:

import {TabPanel,Item as TabPanelItem} from 'devextreme-react/tab-panel.js';
import {locale,loadMessages} from 'devextreme/localization.js';

require需要仿照import实现一个importMap,建立文件映射表,如:

{
  "devextreme-react/tab-panel.js":'/devextreme-react/cjs/tab-panel.js',
  "devextreme/localization.js":'/devextreme-react/cjs/localization.js',
}

但是devextreme组件文件有500个以上,用绝对路径加载的也有好几十个,如果一个个加到importMap中也挺累的。可以实现一个目录映射来一网打尽,如:

"devextreme/":"/devextreme/cjs/",

实现后,发现其实import的importMap也支持这样的目录映射,好多文章没讲。

(2)引用的组件没有扩展名,如:

import {TabPanel,Item as TabPanelItem} from 'devextreme-react/tab-panel';
import {locale,loadMessages} from 'devextreme/localization';

var _file_saver = require("./exporter/file_saver");
var _image_creator = require("./exporter/image_creator");
var _svg_creator = require("./exporter/svg_creator");
var _type = require("./core/utils/type");
var _deferred = require("./core/utils/deferred");
var _pdf_creator = require("./exporter/pdf_creator");

这需要后台的static函数支持这种检查,看看加上.js扩展名是否存在此文件。

(3)引用的组件是个目录。

后台的static要尝试在使用此目录下的index.js文件,如果没有index.js,看看有没有package.json文件,解析使用其中的main属性指向的文件。

组件是目录时,还导致一个额外的问题,浏览器发起者不知道这是个目录,以为是个文件,所以接下来再加载相对路径的组件时,就会导致路径错误,如:

let require('a/b/c'),实际是加载了/a/b/c/index.js,浏览器以为是来自/a/b/c.js,当前目录是/a/b,实际当前目录应该是/a/b/c,如果index.js中有一句require('../d.js'),浏览器就会解析为require('/a/d.js'),导致找不到文件,实际应该是a/b/d.js。

有两种方法解决:

a.后台的实际文件,返回到res的headers的location中。如:res.set('location',req.url);前台获得后,按location值设定当前目录。

b.在importMap中特殊配置一个映射,如:“a/b/c”:"/a/b/c/index.js"

(4)循环引用。就是一个引用文件还没解析执行完,又被其它文件引用到了。devextreme-react中发现2处,引用栈如下:

0: "/devextreme-react/cjs/core/component-base"
1: "/devextreme-react/cjs/core/template-manager"
2: "/devextreme-react/cjs/core/template-wrapper"
3: "/devextreme-react/cjs/core/component-base"
0: "/devextreme-react/cjs/core/component-base"
1: "/devextreme-react/cjs/core/template-manager"
2: "/devextreme-react/cjs/core/component-base"

devextreme中发现1处:

0: "/devextreme/cjs/ui/popup"
1: "/devextreme/cjs/ui/popup/ui.popup.full"
2: "/devextreme/cjs/ui/toolbar"
3: "/devextreme/cjs/ui/toolbar/ui.toolbar"
4: "/devextreme/cjs/ui/toolbar/strategy/toolbar.singleline"
5: "/devextreme/cjs/ui/toolbar/internal/ui.toolbar.menu"
6: "/devextreme/cjs/ui/popup"

为了避免循环引用的死循环栈溢出,require的cache需要在eval前就加入,不能在之后加入。

循环引用实际没有导致devextreme元件出bug,因为export的是一个object,加上js代码执行时的”Hoisting"(变量和函数提升),不马上使用require结果时没有问题。

如:devextreme-reactcjscore emplate-wrapper.js

var component_base_1 = require("./component-base");
...
var TemplateWrapperComponent = function (_a) {
  component_base_1 = require("./component-base");//add by mustapha
  ...
}

第1行代码不能删除,否则可能导致其它组件没有加载进来。TemplateWrapperComponent 中的那行其实也不用加,加了看起来更保险。

(5)require函数返回的module.exports如何处理default?

不要做任何处理,直接返回module.exports。

对于:

import {A} from '/x.js';
import B from '/y.js';

banel转换后,首先会把module.__esModule设置为true,对第2句,会用_interopRequireDefault函数提取default。

var {A}=require('/x.js');
var B=_interopRequireDefault(require('/y.js'));
function _interopRequireDefault(obj) {
    return obj && obj.__esModule ? obj : {
        default: obj
    }
}