ReactJS項目中基于webpack實現頁面插件
整個Web頁面是基于ReactJS的,js打包用的webpack,現在想在Web頁面端實現一種插件機制,可以動態載入第三方寫的js插件。這個插件有一個約定的入口,插件被載入后調用該入口函數,插件內部實現渲染邏輯。插件的實現也使用了ReactJS,當然理論上也可以不使用。預期的交互關系是這樣的:
// 主頁面
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的,否則沒有導出符號:
// webpack.config.js
config.output = {
filename: 'drogo_components.js',
path: path.join(__dirname, 'dist'),
libraryTarget: 'umd',
library: 'drogo_components'
};
此外,為了不打包三方庫進bundle.js,需要設置:
// webpack.config.js
config.externals = {
'react': 'React',
'underscore': '_',
};
externals中key為代碼中require或import xxx from 'xxx'中的名字,value為輸出代碼中的名字。以上設置后,webpack打包出來的代碼類似于:
(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及_模塊。
插件及公共庫如何編寫?
// 入口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中引用時同理。注意需要設置庫的入口文件:
// package.json
"main": "static/js/main.bundle.js",
對于本地庫,可以通過以下方法在本地使用:
// 打包本地庫,生成庫.tgz文件
npm pack
// 切換到要使用該庫的工程下安裝
npm install ../xxx/xxx.tgz
package.json中也不需要依賴該文件,如果不自己install,也是可以在package.json中依賴的,類似:
"xxxx": "file:../xxx/xxx.tgz"
經過以上步驟后,在主頁面中載入插件打包的bundle.js時,會得到錯誤,說找不到React模塊。我這里并沒有完全改造為RequireJS的模塊,所以我在頁面中是靜態引入react的,即:
<script src="static/js/react-with-addons.js"></script>
<script src="static/js/react-dom.min.js"></script>
當執行插件后,RequireJS會去重新載入react.js,如果能load成功,就又會導致瀏覽器環境中出現兩份ReactJS,解決方法是:
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后,要動態載入插件,代碼就類似于:
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/