47

当你敲下weex preview的时候 它在背后都做了什么?

 5 years ago
source link: http://www.zjutkz.net/2018/11/18/当你敲下weex-preview的时候-它在背后都做了什么?/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

当我们在开发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来开发命令行工具,这里我推荐一下阮一峰的 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()) + `http  is 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端可用的两份资源,我们需要做以下几件事:

  1. 使用weex-loader和vue-loader分别对源码进行打包,从而得出两份js构建产物(其实也可以使用同一个loader的,那就是直接使用weex-vue-loader)。
  2. 在web端中使用,需要结合weex-vue-render和vue-runtime这两个js,用以抹平weex和web端的差异(事实上,在注入了这两个js之后,我们可以在全局中得到了一个已经被挂载了的weex实例,而我们可以通过这个weex实例来进行registerModule,registerComponent等操作)。
  3. 移动端很简单,直接使用weex sdk进行渲染就好。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK