0
0

ReactJS项目中基于webpack实现页面插件

Kevin Lynx 发表于 2017年01月08日 00:00 | Hits: 330

整个Web页面是基于ReactJS的,js打包用的webpack,现在想在Web页面端实现一种插件机制,可以动态载入第三方写的js插件。这个插件有一个约定的入口,插件被载入后调用该入口函数,插件内部实现渲染逻辑。插件的实现也使用了ReactJS,当然理论上也可以不使用。预期的交互关系是这样的:

1
2
3
4
5
6
7
8
9
10
// 主页面
load('/plugin/my-plugin.js', function (plugin) {
    plugin.init($('#plugin-main'), args)
})

// 基于ReactJS的插件
function init($elem, args) {
    ReactDOM.render((<Index />), $elem)
}
export {init}

在主页面上支持这种插件机制,有点类似一个应用市场,主页面作为应用平台,插件就是应用,用户可以在主页面上选用各种插件。

问题

目前主页面里ReactJS被webpack打包进了bundle.js,如果插件也把ReactjS打包进去,最终在载入插件后,浏览器环境中就会初始化两次ReactJS。而ReactJS是不能被初始化多次的 。此外,为了插件编写方便,我把一些可重用的组件打包成一个单独的库,让主页面和插件都去依赖。这个库自然也不能把ReactJS打包进来。何况还有很多三方库,例如underscore、ReactDOM最好也能避免重复打包,从而可以去除重复的内容。所以,这里就涉及到如何在webpack中拆分这些库。

需要解决的问题:

  • 拆分三方库,避免打包进bundle.js
  • 动态载入js文件,且能拿到其module,或者至少能知道js什么时候被载入,才能调用其入口函数

关于第二个问题,我选用了RequireJS,但其实它不是用于我这种场景的,不过我不想自己写一个js载入器。用RequireJS在我这种场景下会带来一些问题:webpack在打包js文件时会检查是否有AMD模块加载器,如果有则会把打包的代码作为AMD模块来加载。对于三方库的依赖就需要做一些适配。

实现

开始做这件事时我是不熟悉RequireJS/AMD的,导致踩了不少坑。过程不表,这里就记录一些关键步骤。

公共组件库及插件是必须要打包为library的,否则没有导出符号:

1
2
3
4
5
6
7
// webpack.config.js
config.output = {
  filename: 'drogo_components.js',
  path: path.join(__dirname, 'dist'),
  libraryTarget: 'umd',
  library: 'drogo_components'
};

此外,为了不打包三方库进bundle.js,需要设置:

1
2
3
4
5
// webpack.config.js
config.externals = {
  'react': 'React',
  'underscore': '_',
};

externals中key为代码中require或import xxx from 'xxx'中的名字,value为输出代码中的名字。以上设置后,webpack打包出来的代码类似于:

1
2
3
4
5
6
(function webpackUniversalModuleDefinition(root, factory) {
    if(typeof exports === 'object' && typeof module === 'object')
        module.exports = factory(require("React"), require("_"));
    else if(typeof define === 'function' && define.amd)
        define(["React", "_"], factory);
...

了解了RequireJS后就能看懂上面的代码,意思是定义我这里说的插件或公共库为一个模块,其依赖React及_模块。

插件及公共库如何编写?

1
2
3
4
5
6
7
8
9
10
11
// 入口main.js中
import React from 'react'
import ReactDOM from 'react-dom'
import Test from './components/test'
import Index from './components/index'

function init($elem, data) {
    ReactDOM.render((<Index biz={data.biz} />), $elem)
}

export {Index, Test, init}

入口js中export的内容就会成为这个library被require载入后能拿到的符号。这个库在webpack中引用时同理。注意需要设置库的入口文件:

1
2
// package.json
  "main": "static/js/main.bundle.js",

对于本地库,可以通过以下方法在本地使用:

1
2
3
4
// 打包本地库,生成库.tgz文件
npm pack
// 切换到要使用该库的工程下安装
npm install ../xxx/xxx.tgz

package.json中也不需要依赖该文件,如果不自己install,也是可以在package.json中依赖的,类似:

1
"xxxx": "file:../xxx/xxx.tgz"

经过以上步骤后,在主页面中载入插件打包的bundle.js时,会得到错误,说找不到React模块。我这里并没有完全改造为RequireJS的模块,所以我在页面中是静态引入react的,即:

1
2
<script src="static/js/react-with-addons.js"></script>
<script src="static/js/react-dom.min.js"></script>

当执行插件后,RequireJS会去重新载入react.js,如果能load成功,就又会导致浏览器环境中出现两份ReactJS,解决方法是:

1
2
3
4
5
6
7
8
9
10
11
define('react', [], function() {
  return React
})

define('react-dom', [], function() {
  return ReactDOM
})

define('_', [], function () {
  return _
})

即,因为react被静态引入,就会存在全局变量window.React,所以这里是告诉RequireJS我们定义react模块就是全局变量React。此时webpack中打出的文件中require(['react'], xx时,就不会导致RequireJS再去从服务端载入react.js文件。

使用RequireJS后,要动态载入插件,代码就类似于:

1
2
3
window.require(['/api/plug/content/1'], function (m) {
  m.init($('#app-main')[0], args)
})

最后,之所以没有把页面全部改造为RequireJS,例如通过require载入主页面,主页面依赖react、公共组件库等,是因为我发现RequireJS的载入顺序与项目中使用的部分界面库有冲突,导致一些<a>的事件监听丢失(如下拉菜单不可用),根本原因还没找到。

原文地址:http://codemacro.com/2017/01/08/react-plugin/
written byKevin Lynx posted athttp://codemacro.com

原文链接: http://codemacro.com/2017/01/08/react-plugin/

0     0

评价列表(0)