├── .gitignore ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin └── vvtool ├── lib ├── vvtool.rb └── vvtool │ ├── live_server.rb │ ├── utils.rb │ ├── version.rb │ └── version_checker.rb └── vvtool.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.3 4 | - 2.5 5 | - 2.6 6 | before_install: rm Gemfile.lock 7 | script: "rake build" 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | # Specify your gem's dependencies in vvtool.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | vvtool (0.1.5) 5 | listen (~> 3.0) 6 | rainbow (~> 3.0.0) 7 | rqrcode (~> 0.10.0) 8 | thor (~> 0.20) 9 | 10 | GEM 11 | remote: https://rubygems.org/ 12 | specs: 13 | chunky_png (1.3.11) 14 | ffi (1.9.25) 15 | listen (3.1.5) 16 | rb-fsevent (~> 0.9, >= 0.9.4) 17 | rb-inotify (~> 0.9, >= 0.9.7) 18 | ruby_dep (~> 1.2) 19 | rainbow (3.0.0) 20 | rake (10.5.0) 21 | rb-fsevent (0.10.3) 22 | rb-inotify (0.10.0) 23 | ffi (~> 1.0) 24 | rqrcode (0.10.1) 25 | chunky_png (~> 1.0) 26 | ruby_dep (1.5.0) 27 | thor (0.20.3) 28 | 29 | PLATFORMS 30 | ruby 31 | 32 | DEPENDENCIES 33 | bundler (~> 1.16) 34 | rake (~> 10.0) 35 | vvtool! 36 | 37 | BUNDLED WITH 38 | 1.17.1 39 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 isaced 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VVTool [![Build Status](https://travis-ci.org/isaced/VVTool.svg?branch=master)](https://travis-ci.org/isaced/VVTool) [![Gem](https://img.shields.io/gem/v/vvtool.svg)](https://rubygems.org/gems/vvtool) 2 | 3 | 这是一个加速开发 Virtual View 模版的小脚本,让你能脱离繁重的开发环境 Xcode 和 Android Studio,只需一个轻量级的文本编辑器如 VSCode/Atom/SublimeText 即可开始进入开发,并且提供热加载能力,大大加速提高开发调试效率。 4 | 5 | ![screen_record.gif](https://raw.githubusercontent.com/alibaba/virtualview_tools/master/compiler-tools/RealtimePreview/screenshot.gif) 6 | 7 | ## 安装 8 | 9 | 本工具由 Ruby 所写,你可以通过 Ruby 的包管理工具 `gem` 来安装: 10 | 11 | ```ruby 12 | gem install vvtool 13 | ``` 14 | 15 | > 因为 VV 模版的编译器需要 Java 环境,所以另外需要 java 环境支持。 16 | > 17 | > 如果安装很慢或者超时,可以尝试切换下 RubyGems 源:https://gems.ruby-china.org/ 18 | 19 | [![asciicast](https://asciinema.org/a/rtmYrXUexTG67RNpuGfGdvvGQ.png)](https://asciinema.org/a/rtmYrXUexTG67RNpuGfGdvvGQ) 20 | 21 | ## 运行 22 | 23 | 切换到你的模版列表目录,然后执行如下命令即可: 24 | 25 | ```ruby 26 | vvtool run 27 | ``` 28 | 29 | ## Playground 30 | 31 | 若需要脱离 iOS/Android 开发环境开发 VV,则需要安装对应客户端到真机或模拟器进行预览、调试、开发。 32 | 33 | - [iOS Playground](https://github.com/alibaba/VirtualView-iOS) 34 | - [Android Playground](https://github.com/alibaba/Virtualview-Android) 35 | 36 | > 模拟器:通过 127.0.0.1 访问本机 vvtool 服务 37 | > 38 | > 真机:通过扫描模版对应二维码来访问 39 | > 40 | > 需要运行 VVTool 的机器和对应 Playground 设备都在同一网段; 41 | 42 | ## 模版目录结构 43 | 44 | ``` 45 | . 46 | └── helloworld 47 | ├── helloworld.json (该模版所需参数) 48 | ├── helloworld.out (该模版编译后的二进制) 49 | ├── helloworld.xml (该模版源文件) 50 | └── helloworld_QR.png (该模版 URL 供于扫码加载) 51 | └── helloworld1 52 | ... 53 | ``` 54 | 55 | 你自己需要维持这样一份模版目录结构,才能让服务正确对接到客户端 Playground,其中有几点需要注意: 56 | 57 | 1. 每个模版必须按独立文件夹区分(可以含有子模版) 58 | 2. 模版中的 xml/json 文件名必须和目录名一致 (子模版除外) 59 | 60 | ## 二维码扫描 61 | 62 | 每个模版目录下会生成类似 `xx_QR.png` 的二维码图片,指向当前模版对应的本地HTTP 地址,如 *http://127.0.0.1:7788/helloworld/data.json* ,对应 iOS/Android Playground 应用可通过二维码扫描读取该路径中的模版和数据,然后在客户端加载。 63 | 64 | ## 原理 65 | 66 | ![source](https://i.loli.net/2018/08/02/5b630f232a97e.png) 67 | 68 | > 编译工具依赖 [alibaba/virtualview_tools](https://github.com/alibaba/virtualview_tools) 69 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | task :default => :spec 3 | -------------------------------------------------------------------------------- /bin/vvtool: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "vvtool.rb" -------------------------------------------------------------------------------- /lib/vvtool.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | require "vvtool/version" 4 | require "vvtool/live_server.rb" 5 | require 'Thor' 6 | 7 | 8 | module VVTool 9 | 10 | class CLI < Thor 11 | desc "run", "启动 VirtualView 实时预览服务" 12 | def runLiveServer 13 | VVTool::live_server_run() 14 | end 15 | 16 | desc "about", "关于" 17 | def about 18 | puts "这个命令主要用于 VirtualView 实时预览 - https://github.com/isaced/VVTool" 19 | end 20 | 21 | map %w[--version -v] => :__print_version 22 | desc "--version, -v", "版本" 23 | def __print_version 24 | puts VVTool::VERSION 25 | end 26 | end 27 | end 28 | 29 | VVTool::CLI.start(ARGV) -------------------------------------------------------------------------------- /lib/vvtool/live_server.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | require 'fileutils' 4 | require 'pathname' 5 | require 'webrick' 6 | require 'Listen' 7 | require 'base64' 8 | require 'json' 9 | require 'net/http' 10 | require 'socket' 11 | require 'rqrcode' 12 | require 'rainbow/refinement' 13 | 14 | require_relative 'utils.rb' 15 | require_relative "version_checker.rb" 16 | 17 | using Rainbow 18 | 19 | # 路径 20 | PropertiesFileName = 'templatelist.properties' 21 | ConfigPropertiesFileName = 'config.properties' 22 | CompilerFileName = '.compiler.jar' 23 | TemplatesPath = Dir.pwd 24 | TemplatePath = File.join(TemplatesPath, 'template') 25 | VVBuildPath = File.join(TemplatesPath, 'build') 26 | VVBuildLogFilePath = File.join(TemplatesPath, '.vvbuild.log') 27 | VVCompilerFilePath = File.join(TemplatesPath, CompilerFileName) 28 | VVConfigPropertiesFilePath = File.join(TemplatesPath, ConfigPropertiesFileName) 29 | PropertiesFilePath = File.join(TemplatesPath, PropertiesFileName) 30 | DirFilePath = File.join(TemplatesPath, '.dir') 31 | 32 | # VV 编译器下载地址 URL 33 | VVCompilerDownloadURL = 'https://raw.githubusercontent.com/alibaba/virtualview_tools/bb727ac668856732f66c3845b27646c1b4124fc8/compiler-tools/TemplateWorkSpace/compiler.jar' 34 | # VV 默认的 config.properties 下载地址 URL 35 | VVConfigPropertiesDownloadURL = 'https://raw.githubusercontent.com/alibaba/virtualview_tools/bb727ac668856732f66c3845b27646c1b4124fc8/compiler-tools/TemplateWorkSpace/config.properties' 36 | 37 | # 获取本机 IP 38 | LocalIP = get_first_public_ipv4() 39 | 40 | # HTTP 服务端口号 41 | HTTPServerPort = 7788 42 | 43 | # 编译次数计数 44 | $buildCount = 1 45 | 46 | module VVPrepare 47 | # 拷贝 xml 准备编译 48 | def self.copyXML(copyTemplatePath) 49 | FileUtils.rm_rf TemplatePath 50 | FileUtils.mkdir_p TemplatePath 51 | FileUtils.cp Dir.glob("#{copyTemplatePath}/**/*.xml"), TemplatePath 52 | end 53 | 54 | # 生成 templatelist.properties 文件 55 | def self.generateProperties() 56 | nowTimestamp = Time.now.to_i 57 | propertiesContent = Dir.entries(TemplatePath).reject { |f| File.directory? f } .map { |f| 58 | filename = File.basename f, '.*' 59 | "#{filename}=#{filename},#{nowTimestamp}" 60 | } 61 | File.open(PropertiesFilePath, 'w+') { |f| 62 | propertiesContent.each { |e| f.puts e } 63 | } 64 | end 65 | 66 | # 检查和下载 VV 编译器 67 | def self.checkVVCompiler() 68 | if File.exist? VVCompilerFilePath 69 | puts 'Check VV compiler ok.' 70 | else 71 | puts 'Start downloading VV compiler.jar...' 72 | File.write(VVCompilerFilePath, Net::HTTP.get(URI.parse(VVCompilerDownloadURL))) 73 | if File.exist? VVCompilerFilePath 74 | puts 'VV compiler.jar download success.' 75 | else 76 | puts 'VV compiler.jar download fail.' 77 | exit 78 | end 79 | end 80 | end 81 | 82 | # 检查和下载 config.properties 83 | def self.checkVVConfigProperties() 84 | if File.exist? VVConfigPropertiesFilePath 85 | puts 'Check VV config.properties ok.' 86 | else 87 | puts 'Start downloading VV config.properties...' 88 | File.write(VVConfigPropertiesFilePath, Net::HTTP.get(URI.parse(VVConfigPropertiesDownloadURL))) 89 | if File.exist? VVConfigPropertiesFilePath 90 | puts 'VV config.properties download success.' 91 | else 92 | puts 'VV config.properties download fail.' 93 | exit 94 | end 95 | end 96 | end 97 | 98 | # 编译 99 | def self.vvbuild() 100 | system "java -jar #{VVCompilerFilePath} jarBuild > #{VVBuildLogFilePath}" 101 | end 102 | 103 | def self.generateDataJSON(aTemplatesPath) 104 | # 生成每个模版对应的 data.json 105 | templateNameList = [] 106 | Pathname.new(aTemplatesPath).children.push(aTemplatesPath).each { | aTemplatePath | 107 | templateName = File.basename aTemplatePath, '.*' 108 | 109 | next if not File.directory? aTemplatePath 110 | next if not File.exist?(File.join(aTemplatePath, "#{templateName}.xml")) 111 | next if templateName.start_with? '.' 112 | 113 | # 把所有模版名记录下来 114 | templateNameList << templateName 115 | 116 | # 获取这个模版目录下所有 xml 的编译二进制 Base64 列表 117 | xmlBase64List = [] 118 | Dir.glob(File.join(aTemplatePath, '**/*.xml')).each { | xmlFilePath | 119 | xmlFileName = File.basename xmlFilePath, '.*' 120 | 121 | # 获取这个 xml 的 .out -> base64 122 | xmlBuildOutPath = File.join(VVBuildPath, "out/#{xmlFileName}.out") 123 | xmlBase64String = Base64.strict_encode64(File.open(xmlBuildOutPath, "rb").read) 124 | xmlBase64List << xmlBase64String 125 | } 126 | 127 | # 读取模版参数 JSON 128 | templateParams = {} 129 | templateParamsJSONPath = File.join(aTemplatePath, "#{templateName}.json") 130 | if File.exist?(templateParamsJSONPath) 131 | begin 132 | templateParams = JSON.parse(File.read(templateParamsJSONPath)) 133 | rescue JSON::ParserError => e 134 | puts "[ERROR] - JSON parsing failed! (#{templateName}.json)".red 135 | end 136 | end 137 | 138 | # 合并 data.json (HTTP Server 读取) 139 | if xmlBase64List.count > 0 140 | dataHash = {'templates': xmlBase64List, 'data': templateParams,} 141 | dataJSONPath = File.join(aTemplatePath, "data.json") 142 | File.open(dataJSONPath, "w") { |f| 143 | f.write(JSON.pretty_generate dataHash) 144 | } 145 | end 146 | 147 | # 生成二维码 148 | if LocalIP 149 | qrcode = RQRCode::QRCode.new("http://#{LocalIP}:#{HTTPServerPort}/#{templateName}/data.json") 150 | qrcodeFilePath = File.join(aTemplatePath, "#{templateName}_QR.png") 151 | qrcode.as_png(file: qrcodeFilePath) 152 | end 153 | } 154 | 155 | # 生成模版目录结构 .dir(HTTP Server 读取) 156 | File.open(DirFilePath, "w") { |f| 157 | f.write(JSON.pretty_generate templateNameList) 158 | } 159 | end 160 | 161 | def self.clean() 162 | FileUtils.rm_rf TemplatePath 163 | FileUtils.rm_rf VVBuildPath 164 | FileUtils.rm_f PropertiesFilePath 165 | end 166 | end 167 | 168 | module VVTool 169 | # 第一次 170 | def firstBuild(check_dependency) 171 | # 0. Clean 172 | VVPrepare.clean 173 | 174 | if check_dependency 175 | # 0.1 检查编译器 - 没有则下载 176 | VVPrepare.checkVVCompiler 177 | 178 | # 0.2 检查 config.properties - 没有则下载 179 | VVPrepare.checkVVConfigProperties 180 | end 181 | 182 | puts 'Start build templates...' 183 | 184 | # 1. 拷贝出来集中所有 .xml 模版文件 185 | VVPrepare.copyXML TemplatesPath 186 | 187 | # 2. 生成 compiler.jar 编译所需的 templatelist.properties 文件 188 | VVPrepare.generateProperties 189 | 190 | # 3. 编译 191 | VVPrepare.vvbuild 192 | 193 | # 4. 生成 data.json 194 | VVPrepare.generateDataJSON TemplatesPath 195 | 196 | # 5. Clean 197 | VVPrepare.clean 198 | 199 | puts 'All templates build finished.' 200 | end 201 | 202 | # 单次编译 203 | def singleBuild(aTemplatePath) 204 | # 0. Clean 205 | VVPrepare.clean 206 | 207 | # 1. 拷贝出来集中所有 .xml 模版文件 208 | VVPrepare.copyXML aTemplatePath 209 | 210 | 211 | # 2. 生成 compiler.jar 编译所需的 templatelist.properties 文件 212 | VVPrepare.generateProperties 213 | 214 | # 3. 编译 215 | VVPrepare.vvbuild 216 | 217 | # 4. 生成 data.json 218 | VVPrepare.generateDataJSON aTemplatePath 219 | 220 | # 5. Clean 221 | VVPrepare.clean 222 | end 223 | 224 | def live_server_run(check_dependency = true) 225 | self.firstBuild(check_dependency) 226 | 227 | puts TemplatesPath 228 | # HTTP Server 229 | Thread.new { 230 | http_server = WEBrick::HTTPServer.new( 231 | :Port => HTTPServerPort, 232 | :DocumentRoot => TemplatesPath, 233 | :Logger => WEBrick::Log.new(VVBuildLogFilePath), 234 | :AccessLog => [] 235 | ) 236 | http_server.start 237 | } 238 | 239 | puts "Start HTTP server: " + "http://#{LocalIP || '127.0.0.1'}:#{HTTPServerPort}".blue.bright.underline 240 | 241 | # File Watch 242 | listener = Listen.to(TemplatesPath, only: [/\.xml$/, /\.json$/]) { |modified, added, removed| 243 | (modified + added).each { |filePath| 244 | thisTemplatePath = Pathname.new(filePath).dirname 245 | thisTemplateName = File.basename filePath, '.*' 246 | thisTemplateNameAndExt = File.basename filePath 247 | next if thisTemplateNameAndExt == 'data.json' 248 | puts "[#{ Time.now.strftime("%H:%M:%S") }] Update template: #{thisTemplateName} (#{thisTemplateNameAndExt})" 249 | 250 | self.singleBuild thisTemplatePath 251 | VVPrepare.clean 252 | $buildCount += 1 253 | } 254 | }.start 255 | 256 | puts 'Start Watching...' 257 | puts '' 258 | 259 | trap "SIGINT" do 260 | puts '' 261 | puts "Bye, see you next time, build count: #{$buildCount}" 262 | 263 | # clean 264 | FileUtils.rm_f VVBuildLogFilePath 265 | FileUtils.rm_f DirFilePath 266 | 267 | exit 130 268 | end 269 | 270 | # 从 RubyGems 上检查新版本 271 | check_new_version 272 | 273 | sleep 274 | end 275 | 276 | module_function :live_server_run 277 | module_function :firstBuild 278 | module_function :singleBuild 279 | end -------------------------------------------------------------------------------- /lib/vvtool/utils.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | 3 | def get_first_public_ipv4 4 | ip_info = Socket.ip_address_list.detect{|intf| intf.ipv4? and !intf.ipv4_loopback? and !intf.ipv4_multicast? and !intf.ipv4_private?} 5 | ip_info.ip_address unless ip_info.nil? 6 | end -------------------------------------------------------------------------------- /lib/vvtool/version.rb: -------------------------------------------------------------------------------- 1 | module VVTool 2 | VERSION = "0.1.6" 3 | end 4 | -------------------------------------------------------------------------------- /lib/vvtool/version_checker.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | require 'net/http' 4 | require 'json' 5 | require 'vvtool/version.rb' 6 | 7 | RubyGemsLatestVersionURL = 'https://rubygems.org/api/v1/versions/vvtool/latest.json' 8 | 9 | def get_remote_version 10 | begin 11 | Thread.new { 12 | response = Net::HTTP.get(URI(RubyGemsLatestVersionURL)) 13 | response = JSON.parse(response) 14 | yield response['version'] 15 | } 16 | end 17 | end 18 | 19 | def check_new_version 20 | get_remote_version { |remoteVersion| 21 | currentVersion = VVTool::VERSION 22 | if currentVersion < remoteVersion 23 | puts "VVTool 发现新版本 v#{remoteVersion}(当前 v#{currentVersion}),可以通过命令 `sudo gem install vvtool` 升级" 24 | end 25 | } 26 | end 27 | -------------------------------------------------------------------------------- /vvtool.gemspec: -------------------------------------------------------------------------------- 1 | 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "vvtool/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "vvtool" 8 | spec.version = VVTool::VERSION 9 | spec.authors = ["isaced"] 10 | spec.email = ["isaced@163.com"] 11 | 12 | spec.summary = "VVTool - VirtualView 工具集." 13 | spec.description = "目前可用于结合 VVPlayground 支持 VirtualView 模版开发实时预览." 14 | spec.homepage = "https://github.com/isaced/VVTool" 15 | spec.license = "MIT" 16 | 17 | # Specify which files should be added to the gem when it is released. 18 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 19 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do 20 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 21 | end 22 | 23 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 24 | spec.require_paths = ["lib"] 25 | 26 | spec.add_development_dependency "bundler", "~> 1.16" 27 | spec.add_development_dependency "rake", "~> 10.0" 28 | 29 | spec.add_dependency "rainbow", "~>3.0.0" 30 | spec.add_dependency "thor", "~> 0.20" 31 | spec.add_dependency "listen", "~> 3.0" 32 | spec.add_dependency "rqrcode", "~> 0.10.0" 33 | end 34 | --------------------------------------------------------------------------------