ReactJS項目中基于webpack實現頁面插件

NellKZY 7年前發布 | 15K 次閱讀 React 前端技術 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/

 

 本文由用戶 NellKZY 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!