這個是組內一位同窗在平時開發中,發現調試不便,爲團隊開發的熱更新工具。很厲害,文章中的技術實現內容也是我瞭解了他的具體實現思路後,整理出來的。css
工具源碼EHU(esl-hot-update)html
熱更新就是當你在開發環境修改代碼後,不用刷新整個頁面便可看到修改後的效果。node
若是你的項目中使用了webpack的話,你會很幸運,藉助webpack-dev-server插件能夠實現項目的熱更新。webpack
對於大型的系統級別項目會有下面幾個特色git
模塊化(AMD)模式的普遍使用後,開發環境散文件特別多,很容易上百,一不當心還能上千github
初始化的內容特別多,各類底層庫,ui庫等等web
這兩個特色直接致使每次調試後,刷新會很慢。若是初始化的js達到上千的數量級,每一次從新刷新都是5s,10s,甚至20s的等待。chrome
而熱更新的目的就是爲了在必定程度上減小這5s,10s,甚20s的浪費。express
咱們使用的是百度本身的開發環境工具edp,首先他不支持熱更新npm
咱們使用的AMD實踐也是百度本身的esl,並且即便是requirejs也暫時沒有找到對應的熱更新策略,假如requirejs有對應的,咱們也沒法直接使用
因此最終的結論是咱們本身去實現一個基於咱們本身業務的。這樣咱們考慮的面不用太廣,而且解決方案的更有針對性,即面向咱們現有的業務框架。最重要的是能夠嘗試修改底層框架作配合。
等待路踩通了,咱們再去考慮普適性。
從ehu/package.json 這個文件中,咱們就能夠看出一些具體的思路
須要一個watch功能,即可以監聽到文件的修改
socket.io通知瀏覽器處理文件的改變
修改esl這個文件,達到可以實時更新的效果
當時最簡單的考慮,就是文件改變了後,可以通知瀏覽器,瀏覽器去從新load這個文件而且執行一次。這個時候再從新去打開這個模塊或者功能後,會發現新load的代碼在執行後會覆蓋上一次的。
因此當時的個人第一直覺是,esl重複require時,若是後面一次會覆蓋前面的,那麼能夠經過簡單的覆蓋思路去嘗試,結果發現覆蓋不了。通過驗證,發現是esl內部維護了一個map,即require過的模塊會存起來。咱們若是但願更新這個模塊,只能將map中的對應模塊名刪除。(後面會詳細講述esl的改造)
對應這個工具,我當時也提出了幾個要求
esl必然是須要修改的,可是如何對開發人員透明?首先是不能讓你們都作這種修改。
頁面中也必須加入socket.io支持,那麼咱們如何在不影響其餘人員開發的狀況下加入?
咱們作的屬於beta版本,如何選擇性的使用?ehu工具和之前的開發模式隨意切換?
安裝方便,可否只是做爲一個工具,即插即用,不須要繁瑣的配置?
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命令
工具類: async
和 lodash
先看看昨天對於這個工具提出的幾個要求
esl必然是須要修改的,可是如何對開發人員透明?首先是不能讓你們都作這種修改。
頁面中也必須加入socket.io支持,那麼咱們如何在不影響其餘人員開發的狀況下加入?
咱們作的屬於beta版本,如何選擇性的使用?ehu工具和之前的開發模式隨意切換?
安裝方便,可否只是做爲一個工具,即插即用,不須要繁瑣的配置?
對於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); // ....處理文件修改後對應熱更新邏輯 });
這個原理比較簡單,頁面監聽到樣式的修改,從新加載一次樣式便可,簡單的覆蓋。
可是存在一個潛在問題,由於樣式是簡單的覆蓋,因此,若是修改是刪除了樣式,是沒法生效的。
舉例:
修改前:
display: none; overflow: hidden; position: relative; background: #FFFFFF; border: 1px solid #E8E8E8; margin-top: 20px;
修改後:
display: none; overflow: hidden; position: relative; background: #FFFFFF;
刪除的border
和margin-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
這個是修改後本身實現的控制配置修改的邏輯。
而後這個文件加入到服務端的路由中,請求時替換。
這裏邏輯比較複雜,由於須要修改底層的AMD模塊加載的邏輯。
js沒有模板那麼簡單,不是直接覆蓋,由於在AMD模式中,每個文件,都是被上一個文件調用執行的結果。
因此咱們處理的邏輯是不只須要從新加載修改的文件,而且遞歸全部直接或者間接調用他的文件,所有從新加載。
因此從上面的特色能夠看出,這個工具目前階段主要適用於業務模塊的開發,由於業務的依賴不會特別深,對於dep中的核心文件修改,就不是很合適,一旦文件比較底層,熱跟新是從新加載的模塊也會很是多。
另外也有不少其餘的坑,還在不斷優化中。
此次實踐其實就是業務中遇到的問題(系統太龐大,調試太麻煩),如何解決問題,如何把解決的思路變成一個解決方案,分享給團隊。
由於本身解決了,和造成一個解決方案仍是有很是大的差異的,例如咱們在造成方案的過程當中,就嘗試了不少新東西,踩了不少坑。
目前還有個坑就是chrome瀏覽器,調試的Source資源時,若是一個資源重複加載,內存中會更新,可是對應的資源沒有更新,致使斷點時,映射不對(斷點失效),目前暫時的解決方案是,每次請求時添加時間戳,讓Source映射的資源強制更新。這個能夠正常斷點,可是斷點沒有記憶功能(坑啊,由於文件變了)。