#前書き
先日jphacks2020に参加して、
【frontEngine】というサービスを開発しました。
ファイナリストまで行き、その後あまり良い結果にならず、
ハッカソンとしての反省点などもありますが、
今回に関しては、frontEngineで実装された機能がどのように動いているのかを解説していきます。
今回のものを作成して感じたことは、ジャッジシステムを搭載するよりも、
userからのvue.jsのコードを受け取って、(セキュリティ上)安全にvue.jsを表示できる様になる等プログラミング教材として展開していくと、良いのではないかと考えております。
また、リアクションがどの程度もらえるかわかっていないので、
リアクションがあれば、どんどん記事を精錬させていく所存です。(質問もガンガンください!!)
知らない人もたくさんいると思うので、どういう機能があるかといいますと、
プロジェクト単位(router設定やページ登録)で表現できる!!
デザイン審査もできる(progateみたいにCSSをみているんじゃなくて、CSSの要素 && 要素間の位置をみている)!!
userが書いたコードをそのままプレビューすることができる!!
ジャッジシステムで採点してくれる!!
レーティングが反映される!!(この記事では解説しません)
#概要
今回解説する機能群
frontEngineのデモ
— front engine (@EngineFront) December 7, 2020
プロジェクト単位拡張 pic.twitter.com/quwAXAVTef
— front engine (@EngineFront) December 6, 2020
・ Vue.jsのTemplate部分解析
に挟まれている部分に対して、
こちら側で処理しやすいように解析します。
・ Vue.jsのscript部分解析
に挟まれている部分に対して、jsのように自由に実行できる様に行います。
(ここの部分に)
・ Vue.jsのstyle部分解析
に挟まれている部分に対して、dom側に適用しやすいようにします
・ 上記三つを組み合わせて、vue.js単一ファイルとして機能させる
v-forだったり、v-ifだったりを機能させ、出力できる様にします。
・ プレビュー機能
$mount等で機能させます。
・ プロジェクト単位拡張
現状単一ファイルでしか、読み取れない問題をrouter/index.jsや各ページ単位の問題のように拡張します。
・ ジャッジシステム/スタイルジャッジシステム
正直ここが甘い作りになっていましたが、できる限りやったものを載せます
他にもターミナル機能やレーティングシステムなどもありますが、
それらに関しては私以外のチームメンバーが頑張ってくれたので、チームメンバーが記事を書いてくだされば、リンクを載せます。
##Vue.jsのTemplate部分解析
主にここでソースコードがあります。
domProcess
まず、説明する前に
hanoi
を任意の問題のホームのソースコード入力欄に入れてみて、どのような加工がされるのかをみていきます。
実際に入力すると
{
"open": true,
"close": false,
"name": "div",
"others": [
{
"left": "class",
"right": "exam4",
"directive": false,
"type": "variable",
"variableType": "String",
"variables": [
"exam4"
]
},
{
"left": "class",
"right": "exam4",
"directive": false,
"type": "variable",
"variableType": "String",
"variables": [
"exam4"
]
}
],
"class": {
"left": "class",
"right": "exam4",
"directive": false,
"type": "variable",
"variableType": "String",
"variables": [
"exam4"
]
},
"unique": 0,
"depth": 0,
"children": [
{
"open": true,
"close": false,
"name": "answer-card",
"others": [
{
"left": "class",
"right": "hanoi",
"directive": false,
"type": "variable",
"variableType": "String",
"variables": [
"hanoi"
]
},
{
"left": "@click",
"right": "utusu('left')",
"directive": false,
"type": "function",
"functionTarget": "utusu",
"functionArgument": [
"'left'"
]
},
{
"left": "@click",
"right": "utusu('left')",
"directive": false,
"type": "function",
"functionTarget": "utusu",
"functionArgument": [
"'left'"
]
}
],
"class": {
"left": "class",
"right": "hanoi",
"directive": false,
"type": "variable",
"variableType": "String",
"variables": [
"hanoi"
]
},
"parentId": 0,
"unique": 1,
"depth": 1,
"children": [
{
"open": true,
"close": false,
"name": "answer-card",
"others": [
{
"left": "v-for",
"right": "left",
"directive": true,
"target": {
"value": "value",
"index": "index"
},
"type": "variable",
"variableType": "global"
},
{
"left": "@click",
"right": "trans(value, 'left')",
"directive": false,
"type": "function",
"functionTarget": "trans",
"functionArgument": [
"value",
" 'left'"
]
},
{
"left": "key",
"right": "index",
"directive": true,
"type": "variable",
"variableType": "global"
},
{
"left": "key",
"right": "index",
"directive": true,
"type": "variable",
"variableType": "global"
}
],
"v-for": {
"left": "v-for",
"right": "left",
"directive": true,
"target": {
"value": "value",
"index": "index"
},
"type": "variable",
"variableType": "global"
},
"parentId": 1,
"unique": 2,
"depth": 2,
"children": [
{
"value": "{{value}}",
"reserves": [
{
"start": 0,
"end": 8,
"text": "value",
"textRawValue": "value",
"type": "variable",
"variableType": "global"
}
],
"parentId": 2,
"unique": 3,
"depth": 3,
"name": "reserveText"
}
]
}
]
},
{
"open": true,
"close": false,
"name": "answer-card",
"others": [
{
"left": "class",
"right": "hanoi",
"directive": false,
"type": "variable",
"variableType": "String",
"variables": [
"hanoi"
]
},
{
"left": "@click",
"right": "utusu('center')",
"directive": false,
"type": "function",
"functionTarget": "utusu",
"functionArgument": [
"'center'"
]
},
{
"left": "@click",
"right": "utusu('center')",
"directive": false,
"type": "function",
"functionTarget": "utusu",
"functionArgument": [
"'center'"
]
}
],
"class": {
"left": "class",
"right": "hanoi",
"directive": false,
"type": "variable",
"variableType": "String",
"variables": [
"hanoi"
]
},
"parentId": 0,
"unique": 6,
"depth": 1,
"children": [
{
"open": true,
"close": false,
"name": "answer-card",
"others": [
{
"left": "v-for",
"right": "center",
"directive": true,
"target": {
"value": "value",
"index": "index"
},
"type": "variable",
"variableType": "global"
},
{
"left": "@click",
"right": "trans(value, 'center')",
"directive": false,
"type": "function",
"functionTarget": "trans",
"functionArgument": [
"value",
" 'center'"
]
},
{
"left": "key",
"right": "index",
"directive": true,
"type": "variable",
"variableType": "global"
},
{
"left": "key",
"right": "index",
"directive": true,
"type": "variable",
"variableType": "global"
}
],
"v-for": {
"left": "v-for",
"right": "center",
"directive": true,
"target": {
"value": "value",
"index": "index"
},
"type": "variable",
"variableType": "global"
},
"parentId": 6,
"unique": 7,
"depth": 2,
"children": [
{
"value": "{{value}}",
"reserves": [
{
"start": 0,
"end": 8,
"text": "value",
"textRawValue": "value",
"type": "variable",
"variableType": "global"
}
],
"parentId": 7,
"unique": 8,
"depth": 3,
"name": "reserveText"
}
]
}
]
},
{
"open": true,
"close": false,
"name": "answer-card",
"others": [
{
"left": "class",
"right": "hanoi",
"directive": false,
"type": "variable",
"variableType": "String",
"variables": [
"hanoi"
]
},
{
"left": "@click",
"right": "utusu('right')",
"directive": false,
"type": "function",
"functionTarget": "utusu",
"functionArgument": [
"'right'"
]
},
{
"left": "@click",
"right": "utusu('right')",
"directive": false,
"type": "function",
"functionTarget": "utusu",
"functionArgument": [
"'right'"
]
}
],
"class": {
"left": "class",
"right": "hanoi",
"directive": false,
"type": "variable",
"variableType": "String",
"variables": [
"hanoi"
]
},
"parentId": 0,
"unique": 11,
"depth": 1,
"children": [
{
"open": true,
"close": false,
"name": "answer-card",
"others": [
{
"left": "v-for",
"right": "right",
"directive": true,
"target": {
"value": "value",
"index": "index"
},
"type": "variable",
"variableType": "global"
},
{
"left": "@click",
"right": "trans(value, 'right')",
"directive": false,
"type": "function",
"functionTarget": "trans",
"functionArgument": [
"value",
" 'right'"
]
},
{
"left": "key",
"right": "index",
"directive": true,
"type": "variable",
"variableType": "global"
},
{
"left": "key",
"right": "index",
"directive": true,
"type": "variable",
"variableType": "global"
}
],
"v-for": {
"left": "v-for",
"right": "right",
"directive": true,
"target": {
"value": "value",
"index": "index"
},
"type": "variable",
"variableType": "global"
},
"parentId": 11,
"unique": 12,
"depth": 2,
"children": [
{
"value": "{{value}}",
"reserves": [
{
"start": 0,
"end": 8,
"text": "value",
"textRawValue": "value",
"type": "variable",
"variableType": "global"
}
],
"parentId": 12,
"unique": 13,
"depth": 3,
"name": "reserveText"
}
]
}
]
}
]
}
このようになりました。
みてわかる通り、AST分析を行なっております。
ここで説明のために、
一つのモノ(open,close,name,others,children等を持っている一要素)に対して、
部品と呼ぶことにします。
基本的にゴリゴリっと'<'とか'/>'等を検知して、
BFSやらDFSを行なっております。
Text要素か、dom要素で大きくパターンが変わるのですが、
まず、dom要素からみていきます。
dom要素では、基本的に、':key='だったり'key='だったり'active'等の三パターンに分かれます。
それらの要素をotherとして各要素のothersに格納します。
(=を基準としてみて、左側の属性をleft,右側の属性をrightとしています。)
それに伴い、:であれば、directiveとし、それ以外は文字列型格納として処理します。
(同様に=でない要素に関してはtrueとして格納します)
また、functionかそうでないかは()があるかどうかで、判断できるので、functionであれば、
argumentも別枠として格納しておきます。
次にdom要素として、v-forは、記法が特殊で、left,rightで格納する以外に、前処理してないと扱えたものではないので、ofもしくはinの右側を、通常のrightの部分に格納し、変数部分((value, index) in items のitems,index部分)に関しては、targetというところに格納しておきます。
次にText要素ですが、'{{}}'か生のtextのどちからに分かれると思います。
ただ、もし、{{}}や生のTextが混同しているとしても、dom要素に触れるまで('<name ~...'を見つけるまでは)は、同じ部品として対処していきます。
その部品の中で、あらたにreservesを定義し、またnameを'reserveText'とします。
reservesの中では、{{}}の要素か生のTextかを区別します。
{{で始まれば,}}があるまでは、同一の要素とみなし、生のTextは{{が始まるまでは、生のTextとして処理します。
(startやendなども一応記録として載せていますが、type要素以外domPropery.jsがよしなにしてくれるようにしてます。)
##Vue.jsのscript部分解析
主に下記3つがメインファイルになります
moduleProcess
utility
execScript
script解析に関しては、汎用性が高いと思いますので、npmライブラリ化しようかと考えております。
(公開しました。
https://github.com/imamiya-masaki/engine-js-test
https://www.npmjs.com/package/exec-engine-js)
ので詳しい処理の流れよりもざっくりどのようになっているかを解説致します。
まず、MainProcessから<script></script>内部がmoduleProcessに渡されます。
最初に、module単位でBabelを使いAST化し、各ブロック(data,props,methods,computedなど)に渡され、各々のモノが実行されます。
babelを使ってもAST化されるだけで実行や、変数の変換などは行えません。(仮に行えてもuserからの入力jsをそのまま実行できたらセキュリティ上やばいですよね)
そこで今回は、javascriptの上で動くjavascriptに仕様等が似ている言語のインタプリタを作成していきます。
今回では、propsは別処理としておいているので、dataからみていくことにします。
###data
dataProcess実際にdataProcessの中をみていきます。
import { CheckProperty, getProperty } from './utility.js'
import { global } from '../moduleProcess.js'
export default function (body) {
const output = {}
for (const property of body.body.body[0].argument.properties) {
const getter = getProperty(property)
output[property.key.name] = getter
}
return output
}
引数として、babelでAST化されたdataの中身が渡され、それをfor文で回していきます。
ここで**getProperty**という超便利funcが呼び出されてますが、これは渡されたものを許可された(こちら側で設定した)範囲でjsの値として返してくれます。(実際にconsoleなどは定義してないので、呼び出されることはありません)
key名: value
というdata型で定義されているものを、outputというオブジェクトに定義されているkey名をkeyとして、getPropertyを使いASTから正常な値となったものを該当するkeyのvalueとして格納し、moduleに返します。
###methods
次に、methodsProcessをみていきます。
export default function (body) {
const output = {}
for (const property of body.value.properties) {
output[property.key.name] = property.value
if (output[property.key.name]) {
output[property.key.name].func = true
}
}
return output
}
methodsをみていくと、dataに比べgetPropertyのような、ASTを扱える値に変換するような物がなく、
直接ASTを代入しています。
実際に,上記で使ったhanoiのファイルを流し込んでやると、
hanoiが持っているtransとutusuがoutputに格納されるはずなのでみてみます。
{trans: Node, utusu: Node}
trans: Node {type: "FunctionExpression", start: 356, end: 749, loc: SourceLocation, range: undefined, …}
utusu: Node {type: "FunctionExpression", start: 762, end: 927, loc: SourceLocation, range: undefined, …}
__proto__: Object
となり、試しに展開してみると、
{
"trans": {
"type": "FunctionExpression",
"start": 356,
"end": 749,
"loc": {
"start": {
"line": 20,
"column": 11
},
"end": {
"line": 31,
"column": 5
}
},
"id": null,
"generator": false,
"async": false,
"params": [
{
"type": "Identifier",
"start": 366,
"end": 371,
"loc": {
"start": {
"line": 20,
"column": 21
},
"end": {
"line": 20,
"column": 26
},
"identifierName": "value"
},
"name": "value"
},
{
"type": "Identifier",
"start": 373,
"end": 379,
"loc": {
"start": {
"line": 20,
"column": 28
},
"end": {
"line": 20,
"column": 34
},
"identifierName": "houkou"
},
"name": "houkou"
}
],
"body": {
"type": "BlockStatement",
"start": 381,
...省略...
"directives": []
},
"func": true
},
"utusu": {
"type": "FunctionExpression",
"start": 762,
"end": 927,
"loc": {
"start": {
"line": 32,
"column": 11
...省略...
},
"right": {
"type": "BooleanLiteral",
"start": 908,
"end": 913,
"loc": {
"start": {
"line": 35,
"column": 26
},
"end": {
"line": 35,
"column": 31
}
},
"value": false
}
}
}
],
"directives": []
},
"alternate": null
}
],
"directives": []
},
"func": true
}
}
と、まともに表現することができないほど(したらページが大変なことになってしまう)AST化が完全にされているファイルを格納しています。
methodsが呼ばれるタイミングを考えてみると、DOMで呼ばれるか、もしくはscript内で、
this参照で呼び出しにいく時だと思いますので、その時に便利メソッドgetScriptを呼び出すことで、実行するという仕様にします。
###getScript/execScript
次に、便利メソッドのスタメンであるgetScript/execScriptを紹介します。
実際の中身の詳しい解説に関しては要望があれば致しますが、今回はとりあえずどの様な場面で使われて、どの様な仕組みなのかについて解説致します。
基本的にはgetScriptを使うことにより、他funcからの呼び出しを違和感なく済ませることができます。
function getScript (body, array, preLocal) {
return execScript(body, array, preLocal).returnArguments
}
また、execScriptが処理できる領域は
function (a,b,c) {
//中身
}
このようなブロック単位が存在するfunctionを上手く処理することができます。
さて、次に引数に注目すると、body,array,preLocalという3つを受け取っていることがわかります。
・bodyはmethodsがそのままのASTを保存していたように、bodyではmethodsに格納した様なASTを受け取ります。
・arrayはfunctionの引数を配列型で受け取り、body.params(上記で示した例のa,b,cの部分)をkeyとして、このブロック単位のfunctionが持つローカル変数として格納します
let local = {}
if (array && body.params) {
for (let i = 0; i < body.params.length; i++) {
local[body.params[i].name] = array[i]
}
}
・preLocalは、親block単位要素を引継ぎます。
これがどういうことかと言うと、私が実装しているif文の処理を参考に解説致します。
case 'IfStatement':
let targetGO = access.body[i]
let targetDo = null
while (true) {
if (!targetGO.hasOwnProperty('test')) {
// else
// -> つまりifでないものが全てとおる
targetDo = targetGO
break
}
let resultBool = isBool(targetGO.test, local)
if (!resultBool) {
// false
if (targetGO.alternate) {
targetGO = targetGO.alternate
} else {
break
}
} else {
// true
targetDo = targetGO.consequent
break
}
}
if (targetDo) {
let get = execScript(targetDo, array, local)
Object.keys(get.returnLocal || {}).forEach(key => {
local[key] = get.returnLocal[key]
})
}
break
この処理では、 if(targetDo){}
の中身が、実行されうるif文のblockと考えてください。
そうすると、我々は一番上のblock要素のfunctionの中にいますが、if文の中の結果の変数などは、if文から出ると、棄却されなければいけません。そこを考えると、if文や、for文やwhile文などのblock要素も、一つ下のexecScriptで処理すべき領域と考えます。
そうすると、一つ下の階層(functionから考えて、if文やfor文等)は上の階層の変数などを変更しうる可能性があるので、preLocalの場所に自分のlocal情報を渡し
let local = {}
if (preLocal) {
local = Object.assign(local, preLocal)
}
親のlocalの情報を参照できる様にします。
次に、break文などで終了する場合や単純に、処理が末端まで行えた場合、また親側の処理に戻りますので、
Object.keys(get.returnLocal || {}).forEach(key => {
local[key] = get.returnLocal[key]
})
とし、親側で親側で渡したlocalに対して、更新をかけます。
この更新を行うことにより、子の変数の棄却や、その他の子block内の処理をスムーズに行えます。
これら以外にも、
for文の実装などは変数の都度更新など行っておりますので、よかったらみてくださると嬉しいです。
(isBoolやcalculationやその他の処理もみてくれると嬉しいです。)
case 'ForStatement':
const target = access.body[i]
const initTarget = target.init.declarations[0]
const initName = initTarget.id.name
let initIndex = calculation(initTarget.init, local)
const readyupdate = target.update
let updateCalculation = target.update.right
if (!target.update.right) {
// argument?
updateCalculation = target.update
}
let updateName = ''
if (readyupdate.left) {
updateName = readyupdate.left.name
} else {
// argument
updateName = readyupdate.argument.name
}
const readyBool = target.test
while (isBool(readyBool, { ...local, [initName]: initIndex })) {
let get = execScript(target.body, array, { ...local, [initName]: initIndex })
Object.keys(get.returnLocal || {}).forEach(key => {
if (key !== initName) {
local[key] = get.returnLocal[key]
}
})
if (get.returnOrder === 'break') {
break
}
// updateFunc
if (initName === updateName) {
initIndex = calculation(updateCalculation, { ...local, [initName]: initIndex })
if (initIndex === false) {
console.error('why false!?', updateCalculation, initIndex)
break
}
} else {
local[updateName] = calculation(updateCalculation, { ...local, [initName]: initIndex })
}
}
break
###getProperty/CheckProperty
便利メソッド二つ目のgetProperty/CheckPropertyです。
CheckPropertyは最初の方に作成して、のちにgetPropertyと併用する形となっているので、仕様上少しの違いが生じています(例えば、後述するglobalを自分の子供以降をみてglobalを使っていたり...)
CheckPropertyは、Object,Array,Int,String,Boolなどに対して、値として返してくれるfuncです。
一方getPropertyは、呼び出された場所にあるlocal(上述したfunctionのlocalだったり)や、オブジェクトのキーにアクセスしたり、こちら側で登録したモノ(今回で言えば、ObjectやNumberなど)にアクセスしたり、functionだったらgetScriptとして実行したり、global(vue.js単一ファイル上に存在するthisでアクセスできる場所)を扱ってそうでなければCheckpropertyを呼び出すfunctionです。
###module
もう一度moduleProcessをみてみます。
methodsや、dataやcomputedなどが、先ほど少し話したglobalに格納されていきます。
this参照した時や、domからのアクセスの時にgetPropertyを通して、globalから取り出すことで最適なfunctionだったり、パラメータを取得することができます。
##Vue.jsのstyle部分解析
styleProcess
Vue.jsのstyleとはここでは、vue.jsの<style scpoed></style>内部を指すこととします。
(やっていることは特殊なことはないので、特定ファイルのcssファイルなどにも応用できるかと思います。)
さてここで、vue.jsにはv-bind:style
というものがあります。
これを使えば、後述するプレビュー機能でも、cssに対して特別な設定をすることなく、オブジェクトを渡したら描画することができそうです。
if (val == ' ' || val == '\n' || val == '/s' || val == '↵') {
continue
}
if (val === '{') {
let targetInput = 'tag'
switch (target[0]) {
case '.':
targetInput = 'class'
target.shift()
break
case '#':
targetInput = 'id'
target.shift()
break
}
let targetVal = target.join('')
output[targetInput][targetVal] = {}
i++
let outputKey = []
let value = []
let coron = false
for (;i < style.length; i++) {
if (style[i] == '}') {
// とりあえずclassの階層構造は無視
break
}
if (style[i] != ';') {
if (style[i] == ':') {
coron = true
continue
}
if (!coron) {
if (style[i] == ' ' || style[i] == '\n' || style[i] == '/s' || style[i] == '↵') {
continue
}
outputKey.push(style[i])
} else {
value.push(style[i])
}
} else {
output[targetInput][targetVal][outputKey.join('')] = value.join('')
outputKey = []
value = []
coron = false
}
}
target = []
} else {
target.push(val)
}
短いので掲載しましたが、domProcessの簡略版の様な、
文字列を探索し、オブジェクトに直していきます。
##上記三つを組み合わせて、vue.js単一ファイルとして機能させる
domPreviewParse
MainProcess
ジャッジシステム、プレビュー機能と少し被りますが、
ここでは、プレビュー画面に載せても機能する用に一度ジャッジシステム -> domへのparseを紹介していきます。
まず、最初にジャッジシステムを通さず、templateをAST化したものを読み込むpureDomPreviewParseをみていきます。
function pureDomPreviewParse (domTree, fileName) {
console.log('getDomPreviewParse', domTree)
const output = []
let stack = [domTree]
const parentParam = {}
output.push('<div id="previewDOM">')
while (stack.length > 0) {
const take = stack.pop()
const parseDom = {}
const yoyaku = {}
if (take.hasOwnProperty('class')) {
let targetDom = []
targetDom.push('\'' + fileName + '\'')
if (take.class.hasOwnProperty('variables')) {
take.class.variables.forEach(key => {
targetDom.push('\'' + key + '\'')
})
}
parseDom['v-bind:style'] = 'classEvent(' + targetDom.join(',') + ')'
}
if (take.hasOwnProperty('v-for')) {
const targetValue = []
if (take['v-for'].target.hasOwnProperty('value')) {
targetValue.push(take['v-for'].target.value)
yoyaku[take['v-for'].target.value] = 'value'
}
if (take['v-for'].target.hasOwnProperty('index')) {
targetValue.push(take['v-for'].target.index)
yoyaku[take['v-for'].target.index] = 'index'
}
let targetInput = ''
const targetDom = []
if (take['v-for'].type) {
targetDom.push('\'' + take['v-for'].right + '\'')
targetDom.push('\'' + fileName + '\'')
Object.keys(parentParam).forEach(key => {
targetDom.push('{' + key + ': ' + key + '}')
})
targetInput = 'this.domEvent(' + targetDom.join(',') + ')'
}
// const targetOutput = '(' + targetValue.join(',') + ')' + ' of ' +
parseDom['v-for'] = '(' + targetValue.join(',') + ') of ' + targetInput
}
if (take.hasOwnProperty('others')) {
// 現状v-forとclassを分けたらいけるか...?
for (let i = 0; i < take.others.length; i++) {
if (take.others[i].left === 'v-for' || take.others[i].left === 'class' || take.others[i].left === 'href') {
continue
}
let key = take.others[i].left
if (take.others[i].directive) {
key = ':' + key
}
let targetInput = ''
const targetDom = []
if (take.others[i].type) {
let otherRight = take.others[i].right
otherRight = otherRight.replace(/\'/g, '\\\'')
targetDom.push('\'' + otherRight + '\'')
targetDom.push('\'' + fileName + '\'')
// targetDom.push(Object.keys(parentParam))
Object.keys(parentParam).forEach(key => {
targetDom.push('{' + key + ': ' + key + '}')
})
targetInput = 'domEvent(' + targetDom.join(',') + ')'
}
parseDom[key] = targetInput
}
}
if (take.name === 'reserveText' && take.reserves) {
const textOutput = []
for (let i = 0; i < take.reserves.length; i++) {
const reserveVal = take.reserves[i]
if (reserveVal.type === 'direct') {
textOutput.push(reserveVal.textRawValue)
} else {
const targetDom = []
targetDom.push('\'' + reserveVal.textRawValue + '\'')
targetDom.push('\'' + fileName + '\'')
// targetDom.push(Object.keys(parentParam))
Object.keys(parentParam).forEach(key => {
targetDom.push('{' + key + ': ' + key + '}')
})
textOutput.push('{{ domEvent(' + targetDom.join(',') + ') }}')
}
}
parseDom.reserveText = textOutput.join('')
}
// --各々の作用
if (!parseDom.hasOwnProperty('reserveText')) {
const pushOutput = []
for (let i = 0; i < Object.keys(parseDom).length; i++) {
const key = Object.keys(parseDom)[i]
pushOutput.push(key + '="' + parseDom[key] + '"')
}
let endBlock = '>'
if (!take.open && !take.enClose) {
endBlock = '/>'
}
let startBlock = '<'
if (take.enClose) {
startBlock = '</'
}
if (take.closeParams && take.closeParams.length > 0) {
take.closeParams.forEach(key => {
delete parentParam[key]
})
}
output.push(startBlock + take.name + ' ' + pushOutput.join(' ') + endBlock)
} else {
output.push(parseDom.reserveText)
}
// --parseしてpush
if (take.open) {
const enCloseTag = {}
enCloseTag.name = take.name
enCloseTag.enClose = true
stack.push(enCloseTag)
if (Object.keys(yoyaku).length > 0) {
enCloseTag.closeParams = []
Object.keys(yoyaku).forEach(key => {
parentParam[key] = yoyaku[key]
enCloseTag.closeParams.push(key)
})
}
if (take.hasOwnProperty('children')) {
for (let i = take.children.length - 1; i >= 0; i--) {
const childVal = take.children[i]
stack.push(childVal)
}
}
}
// --子供に対する作用
// --whileEnd
}
output.push('</div>')
return output.join('')
}
基本的には、userがコーディングした、templateをそのままparseしなおせば良いのですが、
scriptだったり、global(style解析)にアクセスするようにしないといけません。
そのため、各々がもっている@clickや、v-for、classなどの対象としている変数をアクセスできるように、
domEvent
やclassEvent
などに変更する(他にもparseEventや、routerEventなどがあります)ことで、のちにプレビューする時に、$vueに、
domEvent: function (order, path, userAction, ...arg) {
let toParam = Object.assign({}, global)
arg.forEach(x => {
toParam = Object.assign(toParam, x)
})
const domPro = domProperty(order, toParam)
if (userAction) {
this.outputDom = domPreviewParse(saveDomTree, path)
this.previewParse()
}
return domPro
},
classEvent: function (path, ...orders) {
// class名を受け取る
let outputObj = {}
orders.forEach(key => {
outputObj = Object.assign(outputObj, globalStyle[path].class[key])
})
return outputObj
},
parseEvent: function (param) {
const splits = param.split('[')
const gets = splits[0]
const index = Number(splits[1].split(']')[0])
if (global[gets]) {
return global[gets][index]
} else {
return false
}
},
routerEvent: function (param) {
this.$emit('router-change', param)
},
などをわたすことで、globalと、template部分との疎通を測ります。
(userActionなどはこの後後述します。)
実際にこれをプレビュー機能に載せようとすると、一つ問題があります。
基本的には、レンダリングやglobalなどの疎通はできるのですが、v-forの内部に@click
などを設定すると、
与えられている要素(例えば配列をv-forで回していて、その配列の中の一つの要素をclickイベントに一つずつ与えてる処理)に対しては、v-forの内部の要素全てクリックしても、最後の要素(配列の最後)のclickeventが発火されます。(詳しい原因はわかってないのですが、参照渡しでdomが渡されていると仮定すれば、最後の要素が参照渡し的に代入されたとすれば、全ての要素が最後の要素になるのは仕方ないのかなと思います (:key
などありますしね...))
そこで、v-for,v-ifやtemplateの動的レンダリングをこちら側で処理することを考えます。
ここで、MainProcessをみてみます。
while (targets.length > 0) {
const ifBool = true
const getTar = targets.pop()
const tar = Object.assign({}, getTar)
if (tar.params) {
// console.lo('targets!!', tar.params, tar)
}
tooru = false
// -- v-for
if (tar['v-for']) {
const target = tar['v-for']
if (target.type === 'variable' || target.type === 'function') {
let data = domProperty(target.right, tar.params)
// とりあえずdataはArray想定 本来ではObjectも考えないといけないよ
if (Array.isArray(data)) {
for (let i = data.length - 1; i >= 0; i--) {
// tar.params = {}
let nextTarget = Object.assign({}, tar)
const params = {}
const textParams = {}
const keys = Object.values(target.target)
params[keys[0]] = data[i]
textParams[keys[0]] = String(keys[0] + '[' + i + ']')
console.log('chh', data[i], data, target)
if (keys.length === 2) {
params[keys[1]] = i
textParams[keys[1]] = i
}
nextTarget.paramIndex = i
nextTarget.paramValue = data[i]
nextTarget.params = Object.assign({}, params)
nextTarget.textParams = Object.assign({}, params)
// console.lo('nextTarget', nextTarget)
delete nextTarget['v-for']
targets.push(nextTarget)
}
} else if (typeof data === 'object') {
// obj
const keys = Object.keys(data)
data = Object.values(data)
for (let i = data.length - 1; i >= 0; i--) {
// tar.params = {}
let nextTarget = Object.assign({}, tar)
const params = {}
const textParams = {}
const keys = Object.values(target.target)
params[keys[0]] = data[i]
console.log('chh', data[i], data)
// textParams[key[0]] =
if (keys.length === 2) {
params[keys[1]] = keys[i]
}
nextTarget.paramIndex = keys[i]
nextTarget.paramValue = data[i]
nextTarget.params = Object.assign({}, params)
// console.lo('nextTarget', nextTarget)
delete nextTarget['v-for']
targets.push(nextTarget)
}
}
continue
} else {
// func
}
}
// 8-- v-for
// v-if
if (tar['v-if']) {
let data = !!domProperty(tar['v-if'].right, tar.params)
if (!data) {
continue
}
}
// 8-- v-if
parseOutput.push(tar)
if (tar.name === 'reserveText') {
// console.log('tarValue:none', tar)
const output = []
for (let reserve of Object.values(tar.reserves)) {
const strValueStart = tar.value.substr(0, reserve.start)
const strValueEnd = tar.value.substr(reserve.end + 1, tar.value.length)
if (reserve.type === 'function') {
const get = domProperty(reserve.textRawValue, tar.params)
// とりあえずglobalのみ対応
const args = []
const toStr = String(get)
output.push(toStr)
} else if (reserve.type === 'variable') {
const get = domProperty(reserve.textRawValue, tar.params)
let toStr = String(get)
// tar.value = strValueStart + toStr + strValueEnd
output.push(toStr)
// console.log('pppRRR', reserve, tar.value, global)
} else if (reserve.type === 'direct') {
output.push(reserve.text)
}
}
tar.value = output.join('')
}
if (option && option.mode === 'answerDOM') {
if (option.existString) {
if (tar.answer && tar.name === 'reserveText') {
// とりあえずexistStringなので....
// console.log('tarValue', tar, targetIndex)
if (typeof lastOutput[outputIndex] !== 'string') {
lastOutput[outputIndex] = ''
}
lastOutput[outputIndex] = lastOutput[outputIndex] + tar.value
} else if (tar.answer) {
// console.log('tarValue:without', tar)
}
// -- lastPropagate
// 子供に伝播
if (tar.name === 'br') {
outputIndex++
}
if (tar.open) {
let closeObject = {}
closeObject.open = false
closeObject.close = true
closeObject.name = tar.name
closeObject.unique = tar.unique
closeObject.depth = tar.depth
targets.push(closeObject)
}
let pushChildren = tar.children || []
for (let i = pushChildren.length - 1; i >= 0; i--) {
const value = pushChildren[i]
let nextObject = {}
nextObject = Object.assign({}, value)
if (tar.hasOwnProperty('params')) {
nextObject.params = Object.assign({}, tar.params)
}
if (tar.hasOwnProperty('textParams')) {
nextObject.textParams = Object.assign({}, tar.textParams)
}
if (tar.hasOwnProperty('paramIndex')) {
nextObject.paramIndex = tar.paramIndex
}
if (tar.hasOwnProperty('answer')) {
nextObject.answer = tar.answer
}
if (tar.name === 'answer') {
nextObject.answer = true
nextObject.answerIndex = i
}
// console.log('cheek', nextObject)
targets.push(nextObject)
// -- lastPrpagate
}
}
}
}
MainProcess(ジャッジシステムをする場所)では、vueのv-forやv-ifが機能しないので、
こちら側で、処理する必要があるので、その機能を少し拝借します。
v-forに注目すると、単純にfor文で回している以外に、params,textParams,parseParamsと、valueとindexを
ASTの中に忍ばせます。
こうすることで、次にdomPreviewParseをみてみると、
function domPreviewParse (domTree, fileName) {
const output = []
const parentParam = {}
saveDomTree = domTree
output.push('<div id="previewDOM">')
const runVueCode = runVueDom(domTree)
// ループが起きると困るので、userアクション(v-on:clickとか@clickとか)の時に最描画するようにする
for (let i = 0; i < runVueCode.length; i++) {
const take = runVueCode[i]
const parseDom = {}
const yoyaku = {}
if (take.name === 'router-link') {
take.routerPush = true
parseDom.routerPush = '@router'
}
if (take.hasOwnProperty('class')) {
let targetDom = []
targetDom.push('\'' + fileName + '\'')
if (take.class.hasOwnProperty('variables')) {
take.class.variables.forEach(key => {
targetDom.push('\'' + key + '\'')
})
}
parseDom['v-bind:style'] = 'classEvent(' + targetDom.join(',') + ')'
}
if (take.hasOwnProperty('others')) {
// 現状v-forとclassを分けたらいけるか...?
for (let i = 0; i < take.others.length; i++) {
if (take.others[i].left === 'v-for' || take.others[i].left === 'class' || take.others[i].left === 'href') {
continue
}
let key = take.others[i].left
if (take.others[i].directive) {
key = ':' + key
}
let targetInput = ''
const targetDom = []
if (take.others[i].type) {
let otherRight = take.others[i].right
otherRight = otherRight.replace(/\'/g, '\\\'')
targetDom.push('\'' + otherRight + '\'')
targetDom.push('\'' + fileName + '\'')
if (key.indexOf('click') >= 0) {
targetDom.push('true')
} else {
targetDom.push('false')
}
// targetDom.push(Object.keys(parentParam))
if (take.parseParams) {
Object.keys(take.parseParams).forEach(key => {
let value = take.parseParams[key]
const valueType = typeof value
if (valueType !== 'number' && valueType !== 'boolean' && valueType !== 'object' && value && (!value.indexOf('[') > 0 && !value.indexOf(']') > 0)) {
value = '\'' + value + '\''
}
if (typeof value === 'string' && value.indexOf('[') > 0) {
value = 'parseEvent(' + '\'' + value + '\'' + ')'
}
if (valueType === 'object') {
const output = []
let stack = [...Object.keys(value)]
// while (stack.length > 0) {
// const takeKey = stack.pop()
// output.push(takeKey + ': ')
// output.push(value[takeKey] + ', ')
// }
for (let i = 0; i < stack.length; i++) {
output.push(stack[i] + ': ')
let outVal = value[stack[i]]
const typeVal = typeof outVal
if (typeof outVal === 'string') {
outVal = '\'' + outVal + '\' '
}
if (i !== stack.length - 1) {
output.push(outVal + ', ')
} else {
output.push(outVal)
}
}
value = output.join('')
value = '\{ ' + value + '\}'
}
targetDom.push('\{' + key + ': ' + value + '\}')
})
}
targetInput = 'domEvent(' + targetDom.join(',') + ')'
}
parseDom[key] = targetInput
}
}
if (take.name === 'reserveText' && take.reserves) {
const textOutput = []
for (let i = 0; i < take.reserves.length; i++) {
const reserveVal = take.reserves[i]
if (reserveVal.type === 'direct') {
textOutput.push(reserveVal.textRawValue)
} else {
const targetDom = []
targetDom.push('\'' + reserveVal.textRawValue + '\'')
targetDom.push('\'' + fileName + '\'')
targetDom.push('false')
if (take.parseParams) {
Object.keys(take.parseParams).forEach(key => {
let value = take.parseParams[key]
const valueType = typeof value
if (valueType !== 'number' && valueType !== 'boolean' && valueType !== 'object' && value && (!value.indexOf('[') > 0 && !value.indexOf(']') > 0)) {
value = '\'' + value + '\''
}
if (typeof value === 'string' && value.indexOf('[') > 0) {
value = 'parseEvent(' + '\'' + value + '\'' + ')'
}
if (valueType === 'object') {
const output = []
let stack = [...Object.keys(value)]
output.push('{ ')
for (let i = 0; i < stack.length; i++) {
output.push(stack[i] + ': ')
let outVal = value[stack[i]]
const typeVal = typeof outVal
if (typeof outVal === 'string') {
outVal = '\'' + outVal + '\''
}
if (i !== stack.length - 1) {
output.push(outVal + ', ')
} else {
output.push(outVal)
}
}
output.push(' }')
value = output.join('')
}
targetDom.push('\{' + key + ': ' + value + '\}')
})
}
textOutput.push('\{\{ domEvent\(' + targetDom.join(', ') + '\) \}\}')
}
}
parseDom.reserveText = textOutput.join('')
}
// --各々の作用
if (!parseDom.hasOwnProperty('reserveText')) {
const pushOutput = []
for (let i = 0; i < Object.keys(parseDom).length; i++) {
const key = Object.keys(parseDom)[i]
if (key === 'routerPush') {
let toParam = {}
if (Object.keys(parseDom).indexOf(':to') >= 0) {
toParam = parseDom[':to']
}
if (Object.keys(parseDom).indexOf('to') >= 0) {
toParam = parseDom['to']
}
if (Object.keys(toParam).length == 0) {
continue
}
pushOutput.push('@click' + '="' + 'routerEvent(' + toParam + ')' + '"')
continue
}
pushOutput.push(key + '="' + parseDom[key] + '"')
}
let endBlock = '>'
if (!take.open && !take.close) {
endBlock = '/>'
}
let startBlock = '<'
if (take.close) {
startBlock = '</'
}
output.push(startBlock + take.name + ' ' + pushOutput.join(' ') + endBlock)
} else {
output.push(parseDom.reserveText)
}
}
console.log('output', output)
output.push('</div>')
return output.join('')
}
先ほどと違い、parseParamsがあるので、それを引数として渡すことで、確実に値渡しとなり、正常に描画されます。
ただ、v-for,v-ifを扱ってないのでvueと同じ様に動的レンダリングするためには、こちら側でレンダリングをさせるようにしないといけません。
レンダリング自体は、globalの値が変更されたものをMainProcess -> domPreviewParseに潜らせれば、レンダリングするので、それをいつ発火させるかです。
今回の場合は、他からの要因である発火イベントで発火するようにしたい(でないと、制約をつけるかしないとレンダリングループしますからね)ので、(今回の場合は)clickイベントとなるような、@click
やv-on:click
にたいして、
if (key.indexOf('click') >= 0) {
targetDom.push('true')
} else {
targetDom.push('false')
}
domEventの第三引数を予約する形にし、ここがtrueの場合処理が終わり次第描画するようにすることで、
元々表現したかった、userのvue.jsコードでの安全な描画ができるようになりました。
##プレビュー機能
デモ:
Homeに流し込んだモノ
プレビュー機能 pic.twitter.com/WVUvBHPayA
— front engine (@EngineFront) December 6, 2020
PreviewField
上記でたいたいの大枠は説明したので、補足として説明いたします。
previewParse: function () {
const getDDD = this.outputDom
const self = this
const domEvent = this.domEvent
const classEvent = this.classEvent
const parseEvent = this.parseEvent
const routerEvent = this.routerEvent
const testSumple = getDDD
bootstrapImports()
let newPreviewDom = Vue.component('newPreviewDom', {
template: getDDD,
methods: {
domEvent: domEvent,
classEvent: classEvent,
parseEvent: parseEvent,
routerEvent: routerEvent
},
components: {
Answer,
PreviewCard,
AnswerCard,
'router-link': Item,
...importBootstrap
}
})
let vm = new Vue({
Answer,
render: h => h(newPreviewDom)
})
// this.pushPreview = newPreviewDom
const targetDomChange = document.getElementById(this.uniqueKey).children[0]
vm.$mount(targetDomChange)
this.$emit('vueDom', vm.$el, vm.$el.children)
}
色々調べると、vue.jsの再$mountの様な記事はあるのですが、
あまりこれと言った記事はでませんでした。
(例えばvueではなく、vueに生の仮想DOMでない生のDOMを載せるモノや、少し古いモノや今回の意図した挙動ではないものが散見しているように思えます)(多分ですが、userのvue.jsをある程度安全に最描画させるという方針でないと、上記のv-for参照渡し問題や、jsのevalセキュリティヤバい問題と衝突するからなのではというのと、vue.jsはそれだけでリッチなので今回の様なプロダクトを目指さなければ他の機能で代用できることが原因かと思われます。)
なので、私と同じ様な問題に衝突した人がいれば、今回のpreview画面は参考になると思われますし、質問があれば是非答えたいと思います。
vue.jsが#appにmountしているように、新たに描画するものにid指定でmountしたいと考えます。
また、今回プロジェクト単位拡張がありますので、idを固定値で指定してしまうと、最初のモノしかレンダリングされないので、propsとして引き受ける様にします。
templateには、上記で行ったvue.jsを再現できるコードにparseしたものを載せ、
methodsには、domEvent,classEvent,parseEvent,routerEventを用意し、これらを踏み台として、globalへのアクセスを可能とさせます。
componentsには、ジャッジシステムのためのAnswer,PreviewCard,AnswerCardと、userがプロジェクト単位拡張問題で書くであろう、'router-view'をこちら側のコンポーネントと紐付けます。
そしてさらにbootstrapを扱えた方がなにかと良いと思ったので、全てをコンポーネントとして読み込むことで、(Vue.use機能は作成したjsインタプリターには機能として載せてないので)表現しています。
##プロジェクト単位拡張
デモ:
Homeに流し込んだソースコード
Exam5Detailに流し込んだソースコード
router設定に流し込んだソースコード
プロジェクト単位拡張 pic.twitter.com/quwAXAVTef
— front engine (@EngineFront) December 6, 2020
一週間の開発期間の後、jphacksでオンラインジャッジシステムを作っているtrack様からこの様なFBを頂きました。
個人的には割と好きなんですが、システムが使えるかどうかという観点で見ると、フロントエンドエンジニア向け製品というにはちょっと遠いですかね。システムとしては、Vue.js (のコンポーネント) のレンダリング結果を評価しているように見えます。逆に言えば、今のシステムはそれしかできなさそう。もちろん、そうした評価もフロントエンド開発における自動テストの一部を担ってはいるのですが、一部でしかなく。
具体的には画面操作を行った時の遷移だとかを評価できそうには見えないので、フロントエンドエンジニアの評価って話としてはまだ風呂敷広げすぎかな、と
ユーザの書いたコードをVueのコンパイラに食わせてUnitTestしているだけだと思う。基本的なアイデアはtrackとまるっきり一緒です。frontendの評価システムとして最近よくあるのは画面のスナップショット取って正解画像との比較をするっていうやり方ですね。progateとかもやってるんじゃなかったかな。
unit(単体)テストやら、画面遷移やら出来ないし、この先も出来ないんじゃないの? (意訳())
じゃ作ってやろうと思いプロジェクト単位の問題を作ることを決心しました。
vue-routerの設定が出来る。それらを使い遷移することが出来る
ができれば単体間の相互作用と、遷移が存在することができれば上記のFBは解決できそうだと感じたので、
これをプロジェクト単位拡張と定義します(実際にはこれら以外にもターミナル機能つけたり、ブラウザのようなタブを増やすことができるようにもしました)
プロジェクト単位に拡張したので、全てのプロジェクト単位の情報を入れる箱が必要なので、
earthというものを定義します globalはもう使っちゃってたので(ぇ
let earth = { pages: {}, targetURL: '/', baseURL: 'localhost:8080', router: {}, earchParam: {}, designChecker: {} } // プロジェクト単位
pagesには、
function pageAdd (pageName, template, script, style, domTree, pure, global) {
earth.pages[pageName] = {}
if (template) {
earth.pages[pageName].template = template
}
if (script) {
earth.pages[pageName].script = script
}
if (style) {
earth.pages[pageName].style = style
}
if (pure) {
earth.pages[pageName].pure = pure
}
if (domTree) {
earth.pages[pageName].domTree = domTree
}
if (global) {
earth.pages[pageName].global = global
}
earth.pages[pageName].pageName = pageName
if (!earth.pages[pageName].hasOwnProperty('url')) {
earth.pages[pageName].url = ''
}
console.log('done:PageAdd!!', earth)
}
新たに提出画面に、ページ追加タブを追加し、
template ~ globalまでの、情報を格納できるようにします。
また、のちにrouterからurlが紐付けられると思うので、それを受け入れられる様にurlを空で初期化しときます。
targetURLは、現在プロジェクト単位問題で表示しているエンドポイントを格納しときます。
baseURLは、今回はlocalhost:8080で初期化しときますが、一応変更できる様に、変数として持っときます。
次にrouterの設定ですが、
routerProcess.jsのrouterProcessに流し込みます。
function routerProcess (text) {
const script = text
const routerAst = routerCreateAST(script)
const astList = routerAst.program.body
const router = {}
console.log('astList', astList)
const pages = Object.assign({}, earth.pages)
const toPages = {} // とりあえず文字列として渡す?
Object.keys(pages).forEach(key => {
toPages[key] = key
})
toPages.Home = 'Home'
toPages.ProblemList = 'ProblemList'
toPages.baseURL = earth.baseURL
for (let i = 0; i < astList.length; i++) {
const value = astList[i]
console.log('value', value)
if (value.type === 'VariableDeclaration') {
for (let i = 0; i < value.declarations.length; i++) {
let decVal = value.declarations[i]
let valInit = decVal.init
if (valInit.type === 'NewExpression') {
// maybeargument one?
valInit = valInit.arguments[0]
let childRouter = {}
console.log('valInit', valInit, value)
for (let i = 0; i < valInit.properties.length; i++) {
if (valInit.properties[i].key.name === valInit.properties[i].value.name && valInit.properties[i].value.type === 'Identifier') {
// routes?
const getRouteKey = valInit.properties[i].key.name
childRouter = Object.assign(childRouter, getProperty(valInit.properties[i].value, toPages))
childRouter[getRouteKey] = routerPushFunc(childRouter[getRouteKey])
continue
}
childRouter[valInit.properties[i].key.name] = getProperty(valInit.properties[i].value, toPages)
}
console.log('router', childRouter, decVal.id.name)
router[decVal.id.name] = childRouter
continue
}
const valRoute = getProperty(valInit, toPages)
console.log('decVal.id.name', decVal.id.name, valRoute, decVal)
router[decVal.id.name] = valRoute
const toRouter = Object.assign({}, router)
toPages[decVal.id.name] = toRouter
}
} else if (value.type === 'ExportDefaultDeclaration') {
earth[value.declaration.name] = router[value.declaration.name]
if (value.declaration.name === 'router') {
// routerの時に特殊な処理
const routerVal = router.router
if (routerVal.routes && routerVal.routes.component) {
for (let i = 0; i < Object.keys(routerVal.routes.component).length; i++) {
const key = Object.keys(routerVal.routes.component)[i]
if (earth.pages[key]) {
const endpoint = routerVal.routes.component[key].path
earth.pages[key].endpoint = endpoint
earth.pages[key].url = routerVal.base + endpoint
render()
}
}
}
outputRouterString = outputRouterInfo()
}
}
}
}
function routerPushFunc (arg) {
const path = {}
const name = {}
const component = {}
let pure = []
pure = [...arg]
for (let i = 0; i < arg.length; i++) {
const val = arg[i]
if (arg[i].hasOwnProperty('name')) {
name[val.name] = val
}
if (arg[i].hasOwnProperty('path')) {
path[val.path] = val
}
if (arg[i].hasOwnProperty('component')) {
component[val.component] = val
}
}
return { path: path, name: name, component: component, pure: pure }
}
今まで同様textベースで貰ったものを、型に落とし込み、
その後、earthのrouterに流し込みます。
また、
export default router
のようにexportされる作りになるはずなので、
これが実行されたら、各々の登録されているページにrouterで設定したrouter情報を紐付けます。
実際のページ遷移がどのように行われているかというと、
オンラインジャッジする時には、レンダリングはしていないので、こちら側でターゲットとしているページを変更すれば良いだけなので解説しませんが、実際にレンダリングを行っているプレビューがどのように行われているかは下記のようになります。
routerChange: function (param) {
// name か pathか調べる
const keys = Object.keys(param)
let params = {}
if (keys.indexOf('params')) {
params.$route = {}
params.$route.params = param.params
}
if (keys.indexOf('name') >= 0 && keys.indexOf('path') >= 0) {
console.error('both name and path exist.')
return false
} else if (keys.indexOf('name') >= 0) {
if (earth && earth.router && earth.router.routes && earth.router.routes.name[param.name]) {
const routeInfo = earth.router.routes.name[param.name]
const componentName = routeInfo.component
if (earth.pages[componentName]) {
this.sumpleTest(earth.pages[componentName].pure, params)
}
}
} else if (keys.indexOf('path') >= 0) {
}
}
router-linkなどは上記三つを組み合わせて、vue.js単一ファイルとして機能させるで、routerEventが設定されるようになってますので、それで上のコードが発火される様になっています。
上のコードは、まずそれがどのような飛び方をするか調べています。nameで飛ぶのかpathで飛ぶのか調べたのち(ハッカソンなのでnameしか実装してないですが、pathも同様に行えば可能)
routerInfoでrouterに格納している情報を取り出し、次に表示するページを準備します。
またvueでのrouterでの遷移は特定のitemを受け渡すことが可能ですので、
if (keys.indexOf('params')) {
params.$route = {}
params.$route.params = param.params
}
としてparamsを渡すことで、相手page側のglobalへ、あげたい情報を流し込むことができます。(これでthis.$route 系の情報を引っ張ることができるようになります。)
(MainProcess汎用的に作っててよかったです...)
これでデモの様に、
ホームに流し込んだソースコードだけで機能するが、
routerとページ追加をして初めて遷移し、$route.paramsの情報を取得できるようになる
を表現することができました。
##ジャッジシステム/スタイルジャッジシステム
デモ:
Homeに流し込んだモノ
デザインチェック pic.twitter.com/P8409jpxkE
— front engine (@EngineFront) December 6, 2020
まず、ジャッジシステムから、みていきます。
最初の一週間で仕上げにいかなければいけなかったので(ファイナリスト選抜があるので)、
文字列の探索のみ実装しています。(それ以降はプレビュー機能やプロジェクト拡張などがあるので、一ヶ月ではこちらに力を割ける時間がなかったです。)
if (option && option.mode === 'answerDOM') {
if (option.existString) {
if (tar.answer && tar.name === 'reserveText') {
// とりあえずexistStringなので....
// console.log('tarValue', tar, targetIndex)
if (typeof lastOutput[outputIndex] !== 'string') {
lastOutput[outputIndex] = ''
}
lastOutput[outputIndex] = lastOutput[outputIndex] + tar.value
} else if (tar.answer) {
// console.log('tarValue:without', tar)
}
// -- lastPropagate
// 子供に伝播
if (tar.name === 'br') {
outputIndex++
}
if (tar.open) {
let closeObject = {}
closeObject.open = false
closeObject.close = true
closeObject.name = tar.name
closeObject.unique = tar.unique
closeObject.depth = tar.depth
targets.push(closeObject)
}
let pushChildren = tar.children || []
for (let i = pushChildren.length - 1; i >= 0; i--) {
const value = pushChildren[i]
let nextObject = {}
nextObject = Object.assign({}, value)
if (tar.hasOwnProperty('params')) {
nextObject.params = Object.assign({}, tar.params)
}
if (tar.hasOwnProperty('textParams')) {
nextObject.textParams = Object.assign({}, tar.textParams)
}
if (tar.hasOwnProperty('paramIndex')) {
nextObject.paramIndex = tar.paramIndex
}
if (tar.hasOwnProperty('answer')) {
nextObject.answer = tar.answer
}
if (tar.name === 'answer') {
nextObject.answer = true
nextObject.answerIndex = i
}
// console.log('cheek', nextObject)
targets.push(nextObject)
// -- lastPrpagate
}
}
}
}
domとして、answerで挟まれているものを見つけ、
それらに対して文字列を見つけます。また正解のものが配列の場合、
があることで、
次のindexのものに進んで見つけます。
このようにして、複数のテストケースに対して正答しているかを調べます。
次にデザインチェックに対してみていきます。
emitDom: function () {
// console.log('previewDom', value, value.children, value.children[0])
const value = this.checkStyleDom
console.log('previewDom:func', value.children[0], value.children[0].children[1].children[0].getBoundingClientRect(), value.children[0].getBoundingClientRect(), [value.children[0]])
console.log('preview:style', value.children[0].children[0].children[0].getBoundingClientRect(), value.children[0].children[1].children[0].getBoundingClientRect(), value.children[0].children[2].children[0].getBoundingClientRect())
let targetStyle = this.getExam.examInfo
let targetBool = true
if (targetStyle && targetStyle.option && targetStyle.option.styleCheck) {
targetStyle = targetStyle.option.styleCheck
} else {
this.checked = true
this.clickFlug = true
return true
}
if (!targetStyle.hasOwnProperty('children')) {
// bugでroot層だけchildrenがないパターン(必要なのに)ないパターンがある
targetStyle.children = {}
Object.keys(targetStyle).forEach(key => {
if (key !== 'count' && key !== 'style' && key !== 'children') {
targetStyle.children[key] = targetStyle[key]
}
})
}
let que = [targetStyle]
let domQue = [value.children[0]]
while (que.length > 0) {
// 正答判定
let take = que.shift()
let countDomTake = []
if (take.count > 0) {
for (let i = 0; i < take.count; i++) {
countDomTake.push(domQue.shift())
}
}
console.log('ccck', take, countDomTake)
const diffStyleCheck = {}
const diffStyles = []
let NextChild = countDomTake[0]
for (let i = 0; i < countDomTake.length; i++) {
let domTake = countDomTake[i]
let domStyle = domTake.getBoundingClientRect()
let domRawStyle = countDomTake[i].style
if (!take.hasOwnProperty('name')) {
// noname
} else {
// nameつき
if (take.name === 'AnswerCard') {
domTake = countDomTake[i].children[0]
NextChild = countDomTake[0].children[0]
}
}
console.log('countDomTake', domTake, domStyle)
diffStyles.push(domStyle)
if (take.hasOwnProperty('style')) {
for (let parentKey of Object.keys(take.style)) {
// _区切りでor判定とする
console.log('take.style', parentKey, take.style)
const splitKeys = parentKey.split('_')
let splitBool = []
for (let i = 0; i < splitKeys.length; i++) {
const key = splitKeys[i]
for (let subKey of Object.keys(take.style[key])) {
console.log('subKey', subKey, domStyle, key)
if (subKey === 'max' || subKey === 'min') {
// 幅指定
if (subKey.match('max')) {
// minの時だけ判定
continue
}
if (domStyle[key]) {
// 他に依存しない
if (take.style[key].min <= domStyle[key] && domStyle[key] <= take.style[key].max) {
continue
} else {
splitBool.push(false)
}
} else {
// 他要素と依存関係にあるstylecheck
diffStyleCheck[parentKey] = true
}
} else if (!(subKey === domRawStyle[key])) {
// absolute指定
if (key === 'overflow') {
// 例外処理
console.log('overflow', key)
const upperSubKey = subKey.toUpperCase()
if (domRawStyle[key + upperSubKey]) {
console.log('overflow', domRawStyle[key + upperSubKey])
} else {
console.log('absolute指定:アウト', subKey, domRawStyle[key], [domRawStyle], [countDomTake[i]])
splitBool.push(false)
this.checkData.reason = "absolute指定:アウト"
this.clickFlug = true
this.reason = "absolute指定:アウト"
}
} else {
console.log('absolute指定:アウト', subKey, domRawStyle[key], [domRawStyle], [countDomTake[i]])
splitBool.push(false)
this.checkData.reason = "absolute指定:アウト"
this.clickFlug = true
this.reason = "absolute指定:アウト"
}
} else {
// trueをいれとく
splitBool.push(true)
}
}
let continueBool = false
for (let take of splitBool) {
if (take) {
continueBool = true
break
}
}
if (continueBool || splitBool.length == 0) {
continue
}
// false
this.checked = false
console.log('style:False', splitBool, take, [domTake])
this.clickFlug = true
return false
}
}
}
}
if (diffStyles.length > 0) {
let xDiffs = [...diffStyles]
let yDiffs = [...diffStyles]
for (let i = 0; i < xDiffs.length; i++) {
xDiffs[i].index = i
yDiffs[i].index = i
}
xDiffs.sort((a, b) => a.x - b.x)
yDiffs.sort((a, b) => a.y - b.y)
for (let i = 1; i < xDiffs.length; i++) {
const xDiff = xDiffs[i].x - (xDiffs[i - 1].x + xDiffs[i - 1].width)
const yDiff = yDiffs[i].y - (yDiffs[i - 1].y + yDiffs[i - 1].height)
xDiffs[i - 1].xDiffRight = xDiff // 右側との差
xDiffs[i].xDiffLeft = xDiff // 左側との差
yDiffs[i - 1].yDiffBottom = yDiff // 下側との差
yDiffs[i].yDiffTop = yDiff // 上側との差
}
let orders = Object.keys(diffStyleCheck)
console.log('orders', orders, diffStyles, countDomTake)
for (let order of orders) {
let splitOrders = order.split('_')
const splitBool = []
console.log('order', order, xDiffs)
for (let key of splitOrders) {
const max = take.style[order].max
const min = take.style[order].min
console.log('checcker', min, max, key)
switch (key) {
case 'padding':
case 'margin':
// とりあえずこれらをまとめてお互いの距離感として処理する
// とりあえず左右だけ見るようにする -> 縦軸も一応取得してるから、見たい時は違う命令で
let marginCheck = true
console.log('paddingOrMargin', xDiffs, key)
for (let i = 0; i < xDiffs.length; i++) {
console.log('xDiffs', xDiffs[i], xDiffs[i].xDiffLeft)
if (xDiffs[i].xDiffLeft || typeof xDiffs[i].xDiffLeft === 'number') {
console.log('xDiffLeft', xDiffs[i])
if (!(min <= xDiffs[i].xDiffLeft && xDiffs[i].xDiffLeft <= max)) {
console.log('style:DiffFalseLeft', key, xDiffs[i], min, max, xDiffs[i].xDiffLeft)
marginCheck = false
break
}
}
if (xDiffs[i].xDiffRight || typeof xDiffs[i].xDiffRight === 'number') {
console.log('xDiffRight', xDiffs[i])
if (!(min <= xDiffs[i].xDiffRight && xDiffs[i].xDiffRight <= max)) {
console.log('style:DiffFalseRight', key, xDiffs[i], min, max, xDiffs[i].xDiffRight)
marginCheck = false
break
}
}
}
splitBool.push(marginCheck)
break
}
}
let checkSplitBool = false
splitBool.forEach(flag => {
if (flag) {
checkSplitBool = true
}
})
if (!checkSplitBool && splitBool.length > 0) {
this.checked = false
this.clickFlug = true
return false
}
}
}
if (NextChild && NextChild.children) {
domQue.push(...NextChild.children)
} else {
}
if (take.hasOwnProperty('children')) {
console.log('take.children', take.children)
que.push(...Object.values(take.children))
}
}
for (let child of value.children[0].children) {
console.log('previewDom:dom', child.children[0], child.children[0].getBoundingClientRect())
}
this.previewDom = value
this.checkFlug = true
this.clickFlug = true
},
デザインチェックボタンでイベント発火し、emitDomが呼ばれます。
ここでは、その問題に紐付けられたスタイルチェック用の情報と照らし合わせにいきますが、
ここでみている点は3つです。
・一つはそれが固有に持たなければ不正解になる要素
(overlayやdisplayなど)
にたいして、そのdomをみて、持っているかどうかを判定します(設定されたものをみるのではなく、レンダリングされたものをみます)
・次に他に依存しないサイズ指定があるもの
(widthやheight)に対して、それがある指定されているサイズに対して、クリアしているかどうかをみます
(例えばwidthやheightの場合、データベースには minとmaxが設定されています。その間に要素のサイズが入ってるかどうかをみます)
・次に他に依存するもの
(paddingやmargin)などに対して、それらの間の距離をみて判断します。
v-forなどで連鎖している要素に対しての、差の情報がminとmaxで入っているので
レンダリングされた情報から取得します。
case 'padding':
case 'margin':
// とりあえずこれらをまとめてお互いの距離感として処理する
// とりあえず左右だけ見るようにする -> 縦軸も一応取得してるから、見たい時は違う命令で
let marginCheck = true
console.log('paddingOrMargin', xDiffs, key)
for (let i = 0; i < xDiffs.length; i++) {
console.log('xDiffs', xDiffs[i], xDiffs[i].xDiffLeft)
if (xDiffs[i].xDiffLeft || typeof xDiffs[i].xDiffLeft === 'number') {
console.log('xDiffLeft', xDiffs[i])
if (!(min <= xDiffs[i].xDiffLeft && xDiffs[i].xDiffLeft <= max)) {
console.log('style:DiffFalseLeft', key, xDiffs[i], min, max, xDiffs[i].xDiffLeft)
marginCheck = false
break
}
}
if (xDiffs[i].xDiffRight || typeof xDiffs[i].xDiffRight === 'number') {
console.log('xDiffRight', xDiffs[i])
if (!(min <= xDiffs[i].xDiffRight && xDiffs[i].xDiffRight <= max)) {
console.log('style:DiffFalseRight', key, xDiffs[i], min, max, xDiffs[i].xDiffRight)
marginCheck = false
break
}
}
}
splitBool.push(marginCheck)
break
}
jphacksの審査員や、チームメンバーすらも
これらの情報の取得がレンダリング後の結果ではなく、
単純にcssをみていると勘違いしていたので、ここだけは強く主張したいです()
(まあ普通に考えたら、cssの方が楽だからそっちで実装したとおもわれるよね..)
#後書き
作成したサービスに対してはある程度説明できたので、ここからは
ハッカソンとしての反省点などを書こうと思います。
また、僕が作ったもの以外もターミナル機能や、マイページ機能、レーティング機能など素晴らしいものがあるので、チームメンバーがそれらに対する記事などを書いてもらったら載せようかと思います。
また再度になりますが、リアクションもらえたり質問をもらえたりすると嬉しいのでLGTMや質問ガンガンもらえたら嬉しいです。よろしくお願いします。
##反省点
まず僕が担当した部分担当していない部分を総括しても、よく完成したプロダクトだと思いますし、
技術力という点に対してもhackdayを取ったサービスや、そのほかの賞を受賞したプロダクトとひけをとらないと思います。
ただ確かな反省点は二点ほどあります。
###ほかのプロダクトと比較して華がない
製品としてほかのものは最新技術を看板にしたものや、
プレゼン込みでプレゼンが華やかになるように製品を作って行った様に思えます。
僕たちのプロダクトはプレゼンで華やかにするのは現状難しいものだと思うので、もう少し上記の様なソースコードの解説や中身をみなくても、わかるようなものをAwardDayで仕上げるべきだったと思います。
###jphacksスポンサー企業との接点がない
僕たちの製品はファイナリストに出場したのですが、その前のHackDay(開発期間一週間でプレゼンする)では、
賞などにかすりもせず、ファイナリストに出場する形となりました。
ファイナリストの出場条件はコード審査(jphacks側での)なので通ったかと思います。
ただ、AwardDayでは、コード審査はなく、プレゼン審査のみとなります。そうなるとスポンサー企業の企業賞というのは効力をもつようになります。
コード審査の場合はjphacks側のみしかみてないので、ファイナリストを他との兼ね合いを考えずにできると思いますがプレゼン審査なので、ある程度他が審議したものの評価もいれないといけません。
そうなると、複数の特定企業に刺さるプロダクトを目指す方向性に、HackDayが終了し次第、うつるべきだったと今では感じます。
僕たちは、貰ったFBを反映する形にしたのですがよくよく考えたら、track開発チーム様は審査員ではないので、そちらのFBを反映するよりも、もう少しペルソナやターゲット層(今回はスポンサー様)に向けるべきだったなと感じます。
ハッカソンというのは、ビジネスソン(isuconとかと違いプロダクトを作りそれを評価するというファジィなものなので)よりな背景もあって然るべきとも思うので、今回は上記のプロダクトとしての華や、ターゲット層の誤りなどビジネス的な観点が今回の大きな敗因だと思います。