如何在前端項目中實現熱更新

圖片描述
這個是組內一位同窗在平時開發中,發現調試不便,爲團隊開發的熱更新工具。很厲害,文章中的技術實現內容也是我瞭解了他的具體實現思路後,整理出來的。css

工具源碼EHU(esl-hot-update)html

熱更新是什麼

熱更新就是當你在開發環境修改代碼後,不用刷新整個頁面便可看到修改後的效果。node

若是你的項目中使用了webpack的話,你會很幸運,藉助webpack-dev-server插件能夠實現項目的熱更新。webpack

解決的問題

對於大型的系統級別項目會有下面幾個特色git

  1. 模塊化(AMD)模式的普遍使用後,開發環境散文件特別多,很容易上百,一不當心還能上千github

  2. 初始化的內容特別多,各類底層庫,ui庫等等web

這兩個特色直接致使每次調試後,刷新會很慢。若是初始化的js達到上千的數量級,每一次從新刷新都是5s,10s,甚至20s的等待。chrome

而熱更新的目的就是爲了在必定程度上減小這5s,10s,甚20s的浪費。express

遇到的問題

  1. 咱們使用的是百度本身的開發環境工具edp,首先他不支持熱更新npm

  2. 咱們使用的AMD實踐也是百度本身的esl,並且即便是requirejs也暫時沒有找到對應的熱更新策略,假如requirejs有對應的,咱們也沒法直接使用

因此最終的結論是咱們本身去實現一個基於咱們本身業務的。這樣咱們考慮的面不用太廣,而且解決方案的更有針對性,即面向咱們現有的業務框架。最重要的是能夠嘗試修改底層框架作配合。

等待路踩通了,咱們再去考慮普適性。

解決的思路

從ehu/package.json 這個文件中,咱們就能夠看出一些具體的思路

  1. 須要一個watch功能,即可以監聽到文件的修改

  2. socket.io通知瀏覽器處理文件的改變

  3. 修改esl這個文件,達到可以實時更新的效果

當時最簡單的考慮,就是文件改變了後,可以通知瀏覽器,瀏覽器去從新load這個文件而且執行一次。這個時候再從新去打開這個模塊或者功能後,會發現新load的代碼在執行後會覆蓋上一次的。

因此當時的個人第一直覺是,esl重複require時,若是後面一次會覆蓋前面的,那麼能夠經過簡單的覆蓋思路去嘗試,結果發現覆蓋不了。通過驗證,發現是esl內部維護了一個map,即require過的模塊會存起來。咱們若是但願更新這個模塊,只能將map中的對應模塊名刪除。(後面會詳細講述esl的改造)

對於工具的要求

對應這個工具,我當時也提出了幾個要求

  1. esl必然是須要修改的,可是如何對開發人員透明?首先是不能讓你們都作這種修改。

  2. 頁面中也必須加入socket.io支持,那麼咱們如何在不影響其餘人員開發的狀況下加入?

  3. 咱們作的屬於beta版本,如何選擇性的使用?ehu工具和之前的開發模式隨意切換?

  4. 安裝方便,可否只是做爲一個工具,即插即用,不須要繁瑣的配置?

如何使用

npm install -g ehu(mac下須要sudo,windows下須要管理員權限)

在原來執行edp webserver start命令的路徑 執行 ehu(再也不須要執行 edp webserver start)

原來端口號8848修改成8844(原8848依舊可使用,但不支持熱更新)

首先使用的方式很簡單,爲此特地將工具打包到npm上,之後就算有升級,僅僅須要你們update便可。

另外從使用角度,也儘可能集成化(一句命令行便可),避免爲了這個工具的使用而作太多額外的事情。

依賴的框架

"dependencies": {
    "async": "^1.5.0",
    "commander": "^2.9.0",
    "express": "^4.13.3",
    "express-http-proxy": "^0.6.0",
    "lodash": "^3.10.1",
    "socket.io": "^1.3.7",
    "watch": "^0.16.0"
  }

幾個必要的
watch——監聽文件變化
socket.io——和瀏覽器的實時通信
express——搭建一個服務
express-http-proxy——代理
commander——便於本身寫node命令

工具類:
asynclodash

框架的思想

先看看昨天對於這個工具提出的幾個要求

  1. esl必然是須要修改的,可是如何對開發人員透明?首先是不能讓你們都作這種修改。

  2. 頁面中也必須加入socket.io支持,那麼咱們如何在不影響其餘人員開發的狀況下加入?

  3. 咱們作的屬於beta版本,如何選擇性的使用?ehu工具和之前的開發模式隨意切換?

  4. 安裝方便,可否只是做爲一個工具,即插即用,不須要繁瑣的配置?

對於1和2,咱們實際上是須要修改/添加一些代碼的,可是代碼都不但願提交到項目的開發環境,由於這些代碼生成環境徹底不須要。

因此咱們的解決方案是:攔截,改寫(偷樑換柱)

舉個例子,當咱們須要對esl作一些改造時,咱們處理方式是當路由指向esl.js時,咱們換成另一個esl-ehu.js(esl-ehu.js是對esl.js改造後的)返回去,這樣就對開發環境的代碼透明瞭。

socket.io的支持也是同理,咱們能夠在返回html時,改寫html的代碼,加入對於socket.io的引入。

上面的思路其實來源於以前項目構建打包。

對於3,咱們但願在使用工具時,任然能很快切換到之前模式,這樣作兼容的目的是但願工具更有競爭力,能吸引你們使用。

咱們的解決方案是:內部實現一個子線程,端口號依然是之前的,並且訪問這個端口,就繞過了這個工具。

對於子線程child_process,咱們還遇到一個問題,就是子線程跑系統的時候,常常掛掉,今天剛剛找到一個解決方案,後面會單開一個文章講這個坑。

對於4,其實就是使用npm方式

技術細節

第一步:搭建一個新服務做爲底層,去託管住咱們如今edp服務,新服務上有一個路由配置,對於咱們須要處理的,攔截。對於不用處理的直接代理給edp

代碼參考

var mid = express();
mid.all('*', httpProxy(config.defaultServer, {
    // 先走特殊規則,不然就代理到默認web server
    filter: function(req, res) {
        return !ruleRoute(req, res);
    },
    forwardPath: function(req, res) {
        return URL.parse(req.url).path;
    }
}));
// 由express-http-proxy託管路由
app.use('/', mid);

ruleRoute就是一些攔截處理

在此以前,啓動下子進程

var child = require('child_process');
var cli = child.exec(defaultServerCLI);
cli.stdout.on('data', function (log) {
    !isServerStarted && (cb(null, log));
    isServerStarted && console.log(log);
});

此處有坑,後面單開文章描述

第二步: 由於上面攔截後的返回的文件已經支持socket.io,esl等底層已經修改了,因此下面是須要去監聽文件通知瀏覽器作對應處理。

// 啓動socket.io服務
io = require('socket.io')(server);
io.on('connection', function (socket) {
    socket.emit('hello');
});
// 監視文件改動
initWatch();

第三步: 作一些集成工做

program
    .version('0.0.6')
    .usage('[options]')
    .option('-p, --port <n>', 'Set the port', setPort)
    .option('-n, --noServerCLI', '...', noServerCLI)
    .parse(process.argv);

集成到node命令中

第四步: 默認配置

module.exports = {
    // 默認的服務器
    defaultServer: 'http://127.0.0.1:8848',
    // 默認的服務器啓動命令
    defaultServerCLI: 'edp webserver start',
    // 從服務器根目錄到須要監控的文件夾中間path
    baseDir: 'nirvana-workspace',
    // hot update 須要watch的文件夾(不包括baseDir)
    watchDirs: 'src',
    // 入口文件(不包括baseDir)
    indexHTML: 'main.html',
    // ehu啓動端口號(不可與默認的服務器端口號衝突)
    port: 8844
};

源碼中有不少邏輯是處理配置的

瀏覽器依賴

socket.io——瀏覽器端僅僅依賴socket這個去和服務端通訊

通訊邏輯

// 創建鏈接
socket.on('hello', function () {
    log(getLogMsgPrefix(), 'HotUpdate已啓動!');
});
// 檢測到文件改動
socket.on('hotUpdate', function (file) {
    // log(getLogMsgPrefix(), '檢測到文件改動', file);
    // ....處理文件修改後對應熱更新邏輯
});

對css/less更新的處理

這個原理比較簡單,頁面監聽到樣式的修改,從新加載一次樣式便可,簡單的覆蓋。

可是存在一個潛在問題,由於樣式是簡單的覆蓋,因此,若是修改是刪除了樣式,是沒法生效的。

舉例:
修改前:

display: none;
overflow: hidden;
position: relative;
background: #FFFFFF;
border: 1px solid #E8E8E8;
margin-top: 20px;

修改後:

display: none;
overflow: hidden;
position: relative;
background: #FFFFFF;

刪除的bordermargin-top實際上是沒有生效的

這個也是後期須要解決的一個問題。

對模板更新的處理

目前項目中使用的是tpl的模板引擎。

如今就遇到一個問題,在熱更新時,模板引擎實際上是重複加載模板的,那麼就涉及到重複加載是否後面的會覆蓋前面問題。

查看加載模板的源碼後,發現根據配置有三個選擇,覆蓋忽略報錯, 咱們業務中使用的配置是遇到重複後會報錯處理,因此咱們須要在不修改業務默認屬性的狀況下,添加一些邏輯。

// [esl-hot-update] 從新加載須要覆蓋
window.EHU_HOT_UPDATE_OPTIONS
&& window.EHU_HOT_UPDATE_OPTIONS.etpl.isOverride
&& (namingConflict = 'override');
switch (namingConflict) {
    /* jshint ignore:start */
    case 'override':
        engine.targets[name] = target;
        context.targets.push(name);
    case 'ignore':
        break;
    /* jshint ignore:end */
    default:
        throw new Error('Target exists: ' + name);
}

window.EHU_HOT_UPDATE_OPTIONS.etpl.isOverride這個是修改後本身實現的控制配置修改的邏輯。

而後這個文件加入到服務端的路由中,請求時替換。

對js更新的處理

這裏邏輯比較複雜,由於須要修改底層的AMD模塊加載的邏輯。

js沒有模板那麼簡單,不是直接覆蓋,由於在AMD模式中,每個文件,都是被上一個文件調用執行的結果。

因此咱們處理的邏輯是不只須要從新加載修改的文件,而且遞歸全部直接或者間接調用他的文件,所有從新加載。

因此從上面的特色能夠看出,這個工具目前階段主要適用於業務模塊的開發,由於業務的依賴不會特別深,對於dep中的核心文件修改,就不是很合適,一旦文件比較底層,熱跟新是從新加載的模塊也會很是多。

另外也有不少其餘的坑,還在不斷優化中。

總結

此次實踐其實就是業務中遇到的問題(系統太龐大,調試太麻煩),如何解決問題,如何把解決的思路變成一個解決方案,分享給團隊。

由於本身解決了,和造成一個解決方案仍是有很是大的差異的,例如咱們在造成方案的過程當中,就嘗試了不少新東西,踩了不少坑。

目前還有個坑就是chrome瀏覽器,調試的Source資源時,若是一個資源重複加載,內存中會更新,可是對應的資源沒有更新,致使斷點時,映射不對(斷點失效),目前暫時的解決方案是,每次請求時添加時間戳,讓Source映射的資源強制更新。這個能夠正常斷點,可是斷點沒有記憶功能(坑啊,由於文件變了)。

微信公衆號

圖片描述

博客地址

http://tangguangyao.github.io/

相關文章
相關標籤/搜索