當你敲下weex preview的時候 它在背後都做了什麼?
當我們在開發Weex的時候,總是會使用到Weex官方的開發工具weex-toolkit,在我們使用weex-toolkit,享受它帶來的便利的時候,還是有必要去了解一下它背後的整個邏輯的,掌握這樣的邏輯有助於我們更好的瞭解如何更好的使用Weex。
我們這篇文章就以weex preview這個最常用的命令為例,來探究一下這背後的程式碼。
weex preview能做什麼
我們以官方的awesome-project為例,當我們在命令列中敲入weex preview src/components/HelloWorld.vue的時候,我們會看到瀏覽器自動打開了一個頁面,這個頁面中一個手機殼一樣的web頁面,讓我們可以檢視Weex程式碼編譯出來的效果,還有一個二維碼,通過playground這樣的掃碼工具進行掃碼,可以在移動端開啟這個Weex頁面。
從這一個頁面我們不難看出一點:同樣的一份程式碼,我們通過一個命令,構建出既適用於web端又適用於移動端的產物,那麼這個產物究竟是如何被構建出來的呢?
構建過程
在開始fuck source code之前,我們要先了解一下如何使用node來開發命令列工具,這裡我推薦一下阮一峰的ofollow,noindex">Node.js 命令列程式開發教程 。
看完文章之後,我們來看weex-toolkit是怎麼做的。根據慣例,我們看weex-toolkit這個專案bin目錄下的js檔案:
xtoolkit.command('preview', 'npm:weex-previewer').locate(require.resolve('weex-previewer'));
其中和我們今天文章相關的一行就是這個,從程式碼中我們知道,preview最終是呼叫了weex-previewer這個npm專案,那我們就再來看weex-previewer的bin目錄裡的js。
detect(program.port).then((open) => { const target = pipe(program.args) if (target) { // If permission to track use let entryCount = 0; let fileType; let options; let optionflags; const entryType = { 2: 'single', 6: 'folder' } if (target.entry) { entryCount+=2; fileType = helper.getFileType(path.basename(target.entry)) } if (target.folder) { entryCount+=4; } optionflags = { entry: !!program.entry, port:!!program.port, verbose:!!program.verbose, config:!!program.config, loglevel:!!program.loglevel } hook.record('/weex_tool.weex-previewer.sence', { file_type: fileType, entry: entryType[entryCount], options: options}); options = { config:program.config } logger.info('Bundling source...') preview(target, open, options); } }) const pipe = (args) => { if (!args || !args[0]) { program.outputHelp(); return false; } const target = args[0]; const ext = path.extname(target); let result = { folder: '', entry: '' } if(!fs.existsSync(target)){ logger.error(`Not found file ${target}`); return false; } if (!ext) { result.folder = target; if (!program.entry) { logger.error(`Need to config the entry file like: \`${binname} ${target} --entry ${path.join(target, 'index.vue')}\``); return false; } else { result.entry = program.entry } } else { result.entry = target || '' } return result; }
其中通過pipe方法去組裝了一個引數,並且呼叫preview方法去處理真正的邏輯。
preview方法的邏輯比較複雜,我們一點一點來看:
init: function (args, port, options) { if (!helper.checkEntry(args.entry)) { return logger.error('Not a ".vue" or ".we" file'); } this.params = Object.assign({}, defaultParams, args); this.params.options = options; this.params.port = port; this.params.wsport = port + 1; this.params.source = this.params.folder || this.params.entry; if (this.params.folder) { this.file = path.relative(this.params.source, this.params.entry); } else { this.file = this.params.entry; } this.fileType = helper.getFileType(this.file); this.module = this.file.replace(path.extname(this.file), ''); this.fileDir = process.cwd(); return this.fileFlow(); }
首先,根據檔名獲取檔案的型別:
getFileType: function (filename) { return /\.vue$/.test(filename) ? 'vue' : 'we'; }
接著,呼叫fileFlow方法:
fileFlow () { logger.verbose(`init template diretory to ${this.params.temDir}`); this.initTemDir(); logger.verbose('building JS file'); this.buildJSFile(() => { logger.verbose('start server'); this.startServer(); }); }
在fileFlow方法中,先呼叫了initTemDir方法:
const WEEX_TMP_DIR = '.weex_tmp'; initTemDir () { if (!fs.existsSync(this.params.temDir)) { this.params.temDir = WEEX_TMP_DIR; fs.mkdirsSync(WEEX_TMP_DIR); fs.copySync(`${__dirname}/../vue-template/template/`, WEEX_TMP_DIR); } // replace old file fs.copySync(`${__dirname}/../vue-template/template/weex.html`, `${this.params.temDir}/weex.html`); const vueRegArr = [{ rule: /{{\$script}}/, scripts: ` <script src="./assets/vue.runtime.js"></script> <script src="./assets/weex-vue-render/index.js"></script> ` }]; const weRegArr = [{ rule: /{{\$script}}/, scripts: ` <script src="./assets/weex-html5/weex.js"></script> ` }]; let regarr = vueRegArr; if (this.fileType === 'we') { regarr = weRegArr; } else { this.params.webSource = path.join(this.params.temDir, 'temp'); if (fs.existsSync(this.params.webSource)) { fs.removeSync(this.params.webSource); } helper.createVueSrc(this.params.source, this.params.webSource); } helper.replace(path.join(`${this.params.temDir}/`, 'weex.html'), regarr); }
可以看到,建立了一個.weex_temp資料夾。各位同學可以在敲下weex preview命令之後去看一下你的工程裡,是會多了一個.weex_temp資料夾的,就是這麼來的。
接著,呼叫了helper的createVueSrc和replace方法,在.weex_temp資料夾下建立了一個weex.html檔案:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>weex-vue-demo</title> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-touch-fullscreen" content="yes"> <meta name="format-detection" content="telephone=no, email=no"> <!-- <style>body > div { height: 100%; }</style> --> <style>body::before { content: "1"; height: 1px; overflow: hidden; color: transparent; display: block; }</style> <script src="./assets/vue.runtime.js"></script> <script src="./assets/weex-vue-render/index.js"></script> </head> <body> <div id="root"></div> <script src="./assets/weex-init.js"></script> </body> </html>
其中最重要的三行是:
<script src="./assets/vue.runtime.js"></script> <script src="./assets/weex-vue-render/index.js"></script> <script src="./assets/weex-init.js"></script>
注入了這三個js檔案,它們是做什麼用的,我們之後再看。
回到weex-previewer,在建立好了.weex_temp之後,呼叫了buildJSFile方法,看名字我們就可以猜到,這就是最關鍵的一步了。
buildJSFile (callback) { const buildOpt = { watch: true, ext: /\.js$/.test(this.params.entry) ? 'js' : this.fileType, ...this.params.options }; let source = this.params.entry; const dest = path.join(this.params.temDir, 'dist'); let webDest; let vueSource = this.params.source; if (this.params.folder) { source = this.params.folder; vueSource = this.params.folder; buildOpt.entry = this.params.entry; } else { webDest = path.join(this.params.temDir, 'dist', this.params.entry.replace(path.basename(this.params.entry), '')); } if (this.fileType === 'vue') { if (buildOpt.entry) { buildOpt.entry = this.params.entry; } else { source = this.params.entry; } // for weex this.build(vueSource, dest, buildOpt, () => { logger.info('weex JS bundle saved at ' + path.resolve(this.params.temDir)); // for web this.build(this.params.webSource, webDest || dest, { web: true, ext: 'js' }, callback); }, () => { // for web this.build(this.params.webSource, webDest || dest, { web: true, ext: 'js' }, callback); }); } else { this.build(source, dest, buildOpt, callback); } }, build (src, dest, opts, buildcallback, watchCallback) { if (!opts.web && path.extname(src) === '.vue') { dest += '/[name].weex.js'; } else if (!opts.web && path.extname(src) !== '.vue') { opts['filename'] = '[name].weex.js'; } builder.build(src, dest, { ...opts, ...this.params.options }, (err, fileStream) => { if (!err) { if (this.wsSuccess) { if (typeof watchCallback !== 'undefined') { watchCallback(); } logger.info(fileStream); server.sendSocketMessage(); } else { buildcallback(); } } else { logger.error(err); } }); }, startServer () { const self = this; server.run({ dir: this.params.temDir, module: this.module, fileType: this.fileType, port: this.params.port, wsport: this.params.wsport, open: this.params.open, wsSuccessCallback () { self.wsSuccess = true; } }); }
這個方法比較長,但是總結起來就是兩點:
(1) 在build方法中呼叫weex-builder去構建js檔案。
(2) startSever方法中起一個http服務。
在weex-builder這個工程中,就是我們喜聞樂見的webpack打包,值得一提的是,其中對weex和web環境進行了區分,weex環境下使用weex-loader打包,而web環境下使用vue-loader打包。打包出來是2個不同的js,一個為html.weex.js,而一個為html.js。
至此,我們知道了整個鏈路中最關鍵的一步:分別使用weex和web的webpack配置去打包同一份原始碼,從而打出兩個不同的目標js檔案。
最後,我們來看startServer。
run (args) { const params = args; const options = { root: params.dir, cache: '-1', showDir: true, autoIndex: true }; this.rootDir = params.dir; if (!this.checkPort(params.port)) { return logger.info('HTTP port is illegal and please try another'); } this.bindProcessEvent(); const servers = httpServer.createServer(options); servers.listen(params.port, '0.0.0.0', () => { logger.info((new Date()) + `httpis listening on port ${params.port}`); const IP = this.getLocalIP(); const previewUrl = `http://${IP}:${params.port}/?hot-reload_controller&page=${params.module}.js&loader=xhr&wsport=${params.wsport}&type=${params.fileType}`; if (params.open) { opener(previewUrl); } logger.info(previewUrl); }); this.startWebSocket(params.wsport, params.wsSuccessCallback); return servers; }
可以看到就是起了一個http服務,根目錄是.weex_temp資料夾。而我們去看.weex_temp資料夾,會發現有一個index.html的入口檔案:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Weex Preview</title> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-touch-fullscreen" content="yes"> <meta name="format-detection" content="telephone=no, email=no"> <link rel="stylesheet" href="./assets/style.css"> <script src="./assets/qrcode.js"></script> <script src="./assets/vue.js"></script> </head> <body> <h1>Weex Preview</h1> <div id="app"></div> <template id="app-template"> <div id="app"> <div class="mock-phone"> <div class="inner"> <iframe id="preview" :src="src"></iframe> </div> <div class="camera"></div> <div class="earpiece"></div> <div class="home-btn"></div> </div> <div id="qrcode"> <h2>QRCode</h2> <a :href="val" target="_blank"><canvas ref="canvas" width="200" height="200"></canvas></a> <p class="bundle-url"><a :href="url" target="_blank">檢視檔案原始碼</a></p> </div> </div> </template> <script> function getUrlParam(key,searchStr) { var reg = new RegExp('[?|&]' + key + '=([^&]+)'); searchStr = searchStr || location.search; var match = searchStr.match(reg) return match && match[1] } var module = getUrlParam('page') || 'app.js'; if(getUrlParam('type') == 'vue') { module = module.replace(/\.js$/,'.weex.js'); } var protocol = location.protocol + '//' var hostname = location.hostname; var wsport = getUrlParam('wsport') || '8082'; var port = location.port ? ':' + location.port : ''; var url = protocol + hostname + port + location.pathname.replace(/\/index\.html$/, '/').replace(/\/$/,'/' + module); new Vue({ el: '#app', template: '#app-template', data: { val: url + '?hot-reload_controller=1&_wx_tpl=' + url, url: url, src: "./weex.html?req=" + Math.floor(Math.random() * 100000) + "&page=" + getUrlParam('page'), }, mounted: function () { var qrcodedraw = new QRCodeLib.QRCodeDraw() qrcodedraw.draw(this.$refs.canvas, this.val.replace('.web',''), function () {}) } }) //for hot reload startSocketCheck(); function startSocketCheck() { if (location.protocol.match(/file/)) { return; } if (location.search.indexOf('hot-reload_controller') === -1) { return; } if (typeof WebSocket === 'undefined') { console.info('auto refresh need WebSocket support'); return; } var host = location.hostname; var port = wsport; try { var client = new WebSocket('ws://' + host + ':' + port + '/', 'echo-protocol'); client.onerror = function () { console.log('refresh controller websocket connection error'); }; client.onmessage = function (e) { console.log('Received: \'' + e.data + '\''); if (e.data === 'refresh') { location.reload(); } }; }catch(er) { console.log(er); } }; </script> </body> </html>
可以看到,程式碼中的ifame就對應文章開頭說的頁面中的手機殼,而二維碼元件則對應可以掃的那個二維碼。
我們先看iframe,iframe中的src引數為:
src: "./weex.html?req=" + Math.floor(Math.random() * 100000) + "&page=" + getUrlParam('page') function getUrlParam(key,searchStr) { var reg = new RegExp('[?|&]' + key + '=([^&]+)'); searchStr = searchStr || location.search; var match = searchStr.match(reg) return match && match[1] }
可以看到,src就是我們前面提到的weex.html。而weex.html中會引用weex-init.js這個檔案:
(function () { function getUrlParam (key) { var reg = new RegExp('[?|&]' + key + '=([^&]+)') var match = location.search.match(reg) return match && match[1] }; var page = getUrlParam('page') || 'index.js'; var bundle = document.createElement('script') // only for web bundle.src = page document.body.appendChild(bundle) })();
這個檔案就是在body中插了一個js檔案而已。而這個js就是前面我們使用weex-builder打包出來的HelloWorld.js。
而這個js之所以能夠被加載出來,就是因為在wee.html中使用了上述提到的weex-vue-render和vue-runtime這兩個元件。
講完了web端,我們回過頭來講一下移動端。移動端非常簡單,其實就是二維碼關聯了HelloWorld.weex.js這個檔案,在掃碼的時候通過移動端sdk進行載入而已。
總結
通過上面的分析,我們可以知道如果需要將一份weex的原始碼構建出在移動端和web端可用的兩份資源,我們需要做以下幾件事:
- 使用weex-loader和vue-loader分別對原始碼進行打包,從而得出兩份js構建產物(其實也可以使用同一個loader的,那就是直接使用weex-vue-loader)。
- 在web端中使用,需要結合weex-vue-render和vue-runtime這兩個js,用以抹平weex和web端的差異(事實上,在注入了這兩個js之後,我們可以在全域性中得到了一個已經被掛載了的weex例項,而我們可以通過這個weex例項來進行registerModule,registerComponent等操作)。
- 移動端很簡單,直接使用weex sdk進行渲染就好。