Reactjs+BootStrap开发自制编程语言Monkey的编译器:词法解析1
2017-11-10 11:58
766 查看
我们先看一句简单的代码:
编译器在解析这条语句前,它需要做一项分析工作,它会把上面的语句各个要素进行分类如下:
1:let
2: x , y
3:=
4:+,
5:5
6:;
也就说 编译器把一句代码中的不同元素分成了六组,第一组是由关键字’let’组成的集合;第二组是三个字符串或是字符的集合;第三组由等于号’=’组成;第四组是一个个特殊符号’+’组成的集合;第五组是由数字‘5’组成的集合;第六组是符号’;’独自组成的一个集合;为了区分不同的集合,我们为每一个集合赋予一个不同的值,第一组赋值0,第二组赋值1,依次类推,第六组赋值5。直接赋与数值不利于人的理解,于是我们可以用编程中常量定义的方法,用不同的常量来对应不同的值,例如:
经过分类后,上面的代码语句在编译器的眼里就变成了:
LET IDENTIFIER EQUAL_SIGN IDENTIFIER PLUS_SIGN INTEGER SEMICOLON
于是我们完成了对代码编译时的第一步抽象。分类的一个原则是,所有关键字自己单独成为一类,后面我们要看到的关键字例如 if else 他们会自己成为一类,所有表示变量的字符串,例如x, y, monkey, 等全部被划入IDENTIFIER一类,所有的特殊符号,例如’:’,’-‘,’*’,’/’等,都自己形成一类,所有的数字字符串例如’5’, ‘123’,等全部划入INTEGER这类。因此经过第一层处理后,编译器看到的再也不是具体的字符,而是代码中不同元素所对应的分类。
然而仅仅向上面那样分类,那很多信息就丢失了,这样编译器在后面就不能就顺利的解释执行或是代码生成,因此除了分类后,我们还必须附带上必要信息,例如对于分类IDENTIFIER, 我们还需要附带上它对应的字符串,对于分类INTEGER,我们还需要附带上它对应的数值,最好还是要附带上该元素所在的行号,这样以便于输出错误信息或者开发调试器。于是上面的解析再次增强为:
{type: LET, literal: “let”, lineNumber: 0} {type: EQUAL_SIGN, literal:”=”, lineNumber: 0} {type: IDENTIFIER, literal: “x”, lineNumber: 0} {type: EQUAL_SIGN, literal:”=”, lineNumber: 0} {type: IDENTIFIER, literal:”y”} {type: PLUS_SIGN, literal: “+”, lineNumber: 0} {type: INTEGER, literal:”5”, lineNumber: 0} {type: SIMICOLON, literal: “;”, lineNumber: 0}
编译器把代码转变为上面这种结构的过程,就叫词法解析。其中类似{type: LET, literal: “let”, lineNumber: 0} 这种结构体呢,我们就叫Token.我们在src/目录下新建一个组件文件叫MonkeyLexer.js,它将专门用来实现词法解析的功能,然后先在该文件中添加Token对象的定义:
类MonkeyLexer将负责把源代码解析成一系列Token的组合。词法解析的基本办法是,先把字符一个个读出来,判断一下读到的单个字符是否是特殊符号,例如’;’, ‘+’等,如果是,那么直接生成对应的Token对象,如果不是,那么就把字符攒起来,直到遇到空格,回车换行为止,接着判断一下攒起来的字符串是关键字,还是变量,还是整形数值,根据不同情况生成不同Token对象。我们看看解析算法的代码是如何实现的:
MonkeyLexer 是词法解析器,在他的初始化构造函数constructor中,它调用initTokenType函数,先为不同的元素分类给定一个唯一整数以便加以区分。接着我们需要一个函数,以便把字符从代码字符串中一个个读出来,这个函数实现如下:
readChar() 从代码字符串中逐个读取字符,每读取一个字符,让readPosition加一,每次读取时,代码总是从readPoisition指向的位置开始读取。skipWhiteSpaceAndNewLine函数的作用是,判断读取的字符是不是空格,如果是空格,那么就忽略当前读取的字符,继续读取后续字符,如果字符是回车换行,那么把表示当前行号的变量lineCount加1,然后继续往后读取,直到读取到不是空格,或是回车换行字符为止。当读取到有效字符之后,我们要根据字符的含义把它归类,例如当读取到的字符是’;’时,就创建一个类型为SEMICOLON的Token对象,具体代码实现如下:
nextToken函数在执行时,先通过调用skipWhiteSpaceAndNewLine,滤掉空格以及回车换行等特殊字符,一旦独到有效字符后,进入switch部分,如果当前的字符是特殊字符,例如’;’,’=’,’+’等,由于这些字符各自属于单独一个分类,因此分别给他们创建里一个Token对象,如果读到的是普通英文字符或者是数字字符,那么就进入default代表的代码处。
当代码连续读入的字符是普通英文字符或是数字字符时,词法解析器会把这些字符凑成一个字符串,假设读入的代码是:
那么解析器读入上面语句时,首先它会连续读入5个字符: f, i, v, e,然后把他们组合成一个字符串”five”,接着为该字符串生成一个分类为IDENTIFIER的Token对象,当解析器读入’=’后面的内容时,它会把后面的数字字符分别读入,也就是分别读取’1’,’2’,’3’三个字符,然后把这三个字符组合成字符串”123”,最后给这个字符串创建一个类型为INTEGER的Token对象。这些工作分别由函数readIdentifier() 和 函数 readNumber()来实现,我们看看他们的代码:
readIdentifier 在执行时,先调用isLetter来判断当前读入的字符是否是字母,如果是,那么它就把所有字符集合起来,形成一个字符串。readNumber在执行时,它先判断当前读入的字符是否是数字,如果是,它就把所有数字字符集合起来,形成一个数字组成的字符串。在nextToken的switch语句部分,如果逻辑进入default部分,那么函数会调用readIdentifier()看看当前是否读到了一个由字母组合成的字符串,如果是,那么就创建一个类型为IDENTIFIER的Token对象,如果不是由字母组成的字符串,那么就接着调用readNumber看看当前内容是不是全是由数字组成的字符串,如果是,那么就创建一个类型为INTEGER的Token对象,如果不是,那说明当前读到了词法解析器无法理解的字符,因此返回一个undefined对象。
更详细的讲解和代码调试演示过程,请点击链接
到目前为止,我们的词法解析部分已经基本成型了,现在就看如何调用起MonkeyLexer这个组件,以便用来分析在页面文本框中输入的代码。要想运行MonkeyLexer这个组件,我们需要把页面文本框中的内容得到,然后传入到该组件中。
回到MonkeyCompilerIDE.js文件,页面加载时,该文件里的MonkeyCompilerIDE.render 函数会被调用,以便用于渲染页面。render在执行时返回了一个JSX对象,其中有一个控件是这样的:
上面这个控件的作用就是在页面上创建出一个输入文本框。当用户在文本框上输入内容后,点击下面的红色按钮,我们如何得到框内的文本内容呢?要想实现这个功能,我们必须要获得控件的实例对象,把上面的控件代码做如下修改:
注意看,我们增加了部分代码如下:
inputRef是Reactjs给我们提供的指令,如果一个控件,如果它要想在页面上绘制或是创建内容的话,它必须实现一个render()接口,render()接口会被reactjs框架调用,于是组件就可以在render中去绘制页面,那么render()是如何被reactjs调用的呢?当一个组件被放入到”<>”,这两个尖括号中时,reactjs解析到后就会自动把尖括号里面的组件对象得到,然后调用它的reander函数。例如上面代码中,夹在尖括号中的组件叫bootstrap.FormControl, 那么reactjs在解析到上面代码时,会自动调用bootstrap.FormControl.render(),于是一个输入文本框就会显示到页面上了。
如果要想把尖括号包围起来的组件对象获取到,就得依靠inputRef指令,就像我们上面做的那样,当reactjs解读尖括号中的组件时,如果发现其中包含inputRef指令,那么他就会执行后面大括号里面的代码,上面代码中,ref变量就是reactjs框架传给我们的组件对象,其中this指向的是MonkeyCompilerIDE这个组件对象本身,this._textAreaControl = ref 它的意识是,在MonkeyCompilerIDE这个对象内部创建一个名为_textAreaControl的成员变量,然后把ref指向的控件对象赋值给它,这样我们就可以获得文本框控件的实例对象,有了实例对象,我们通过访问它的value属性就可以获得文本框内的文本了。
接下来我们需要关注的是如何响应底层按钮的点击。在JSX中,对应按钮的组件是:
上面的代码经过reactjs解析后会在页面上绘制出底部那个红色的按钮,其中bsStyle=”danger” 称之为组件的属性,是用来从将信息从外部传入组件内部的,后面我们会详细讲解这个特性。如何响应按钮的点击时间呢?如下:
我们增加对onClick事件的捕捉,一旦用户点击按钮后,onClick事件被触发,它会调用我们自己实现的onLexingClick函数,这里一定要使用bind把onLexingClick绑定,要不然被调用时,this指针不指向MonkeyCompilerIDE组件。我们再看看响应函数的实现:
我们先通过new 构建一个MonkeyLexer实例,this._textAreaControl.value对应文本框中输入的代码内容,并把创建的实例赋值给当前组件的lexer成员变量,最后调用MonkeyLexer导出的lexing函数开始词法解析流程。
上面代码完成后,加载页面,在文本框中输入几句代码,点击按钮进行词法解析,结果如下:
我在左边输出了两条语句:
右边控制台输出了词法解析的结果,其中变量”five”形成的Token对象中,分类为1,对应我们的代码,它就是IDENTIFIER, 第二行的数字6,它对应的Token中,分类值为4,对应到代码中是NUMBER,并且它所在的行号是1,从这两处结果看,词法解析的结果基本正确。但有个问题就是let, 它对应的Token中分类是1,对应的就是IDENTIFIER, 这是有问题的,前面我们说过,let是关键字,它必须对应自己的分类,因此词法解析在这里出了点问题,下一节,我们再处理它。
更详细的讲解和代码调试演示过程,请点击链接
如果点击后视频还没有,那表明视频还在云课堂的审查过程中,敬请期待。
更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号:
let x = y + 5;
编译器在解析这条语句前,它需要做一项分析工作,它会把上面的语句各个要素进行分类如下:
1:let
2: x , y
3:=
4:+,
5:5
6:;
也就说 编译器把一句代码中的不同元素分成了六组,第一组是由关键字’let’组成的集合;第二组是三个字符串或是字符的集合;第三组由等于号’=’组成;第四组是一个个特殊符号’+’组成的集合;第五组是由数字‘5’组成的集合;第六组是符号’;’独自组成的一个集合;为了区分不同的集合,我们为每一个集合赋予一个不同的值,第一组赋值0,第二组赋值1,依次类推,第六组赋值5。直接赋与数值不利于人的理解,于是我们可以用编程中常量定义的方法,用不同的常量来对应不同的值,例如:
const LET = 0; const IDENTIFIER = 1; const EQUAL_SIGN = 2; const PLUST_SIGN = 3; const INTEGER = 4; const SEMICOLON = 5;
经过分类后,上面的代码语句在编译器的眼里就变成了:
LET IDENTIFIER EQUAL_SIGN IDENTIFIER PLUS_SIGN INTEGER SEMICOLON
于是我们完成了对代码编译时的第一步抽象。分类的一个原则是,所有关键字自己单独成为一类,后面我们要看到的关键字例如 if else 他们会自己成为一类,所有表示变量的字符串,例如x, y, monkey, 等全部被划入IDENTIFIER一类,所有的特殊符号,例如’:’,’-‘,’*’,’/’等,都自己形成一类,所有的数字字符串例如’5’, ‘123’,等全部划入INTEGER这类。因此经过第一层处理后,编译器看到的再也不是具体的字符,而是代码中不同元素所对应的分类。
然而仅仅向上面那样分类,那很多信息就丢失了,这样编译器在后面就不能就顺利的解释执行或是代码生成,因此除了分类后,我们还必须附带上必要信息,例如对于分类IDENTIFIER, 我们还需要附带上它对应的字符串,对于分类INTEGER,我们还需要附带上它对应的数值,最好还是要附带上该元素所在的行号,这样以便于输出错误信息或者开发调试器。于是上面的解析再次增强为:
{type: LET, literal: “let”, lineNumber: 0} {type: EQUAL_SIGN, literal:”=”, lineNumber: 0} {type: IDENTIFIER, literal: “x”, lineNumber: 0} {type: EQUAL_SIGN, literal:”=”, lineNumber: 0} {type: IDENTIFIER, literal:”y”} {type: PLUS_SIGN, literal: “+”, lineNumber: 0} {type: INTEGER, literal:”5”, lineNumber: 0} {type: SIMICOLON, literal: “;”, lineNumber: 0}
编译器把代码转变为上面这种结构的过程,就叫词法解析。其中类似{type: LET, literal: “let”, lineNumber: 0} 这种结构体呢,我们就叫Token.我们在src/目录下新建一个组件文件叫MonkeyLexer.js,它将专门用来实现词法解析的功能,然后先在该文件中添加Token对象的定义:
class Token { constructor(type, literal, lineNumber) { this.type = type this.literal = literal this.lineNumber = lineNumber } type() { return this.type } literal() { return this.literal } lineNumber() { return this.lineNumber } } class MonkeyLexer { constructor(sourceCode) { this.sourceCode = sourceCode } } export default MonkeyLexer
类MonkeyLexer将负责把源代码解析成一系列Token的组合。词法解析的基本办法是,先把字符一个个读出来,判断一下读到的单个字符是否是特殊符号,例如’;’, ‘+’等,如果是,那么直接生成对应的Token对象,如果不是,那么就把字符攒起来,直到遇到空格,回车换行为止,接着判断一下攒起来的字符串是关键字,还是变量,还是整形数值,根据不同情况生成不同Token对象。我们看看解析算法的代码是如何实现的:
class MonkeyLexer { constructor(sourceCode) { this.initTokenType() this.sourceCode = sourceCode this.poistion = 0 this.readPosition = 0 this.lineCount = 0 this.ch = '' } initTokenType() { this.ILLEGAL = -2 this.EOF = -1 this.LET = 0 this.IDENTIFIER = 1 this.EQUAL_SIGN = 2 this.PLUS_SIGN = 3 this.INTEGER = 4 this.SEMICOLON = 5 } .... }
MonkeyLexer 是词法解析器,在他的初始化构造函数constructor中,它调用initTokenType函数,先为不同的元素分类给定一个唯一整数以便加以区分。接着我们需要一个函数,以便把字符从代码字符串中一个个读出来,这个函数实现如下:
class MonkeyLexer { .... readChar() { if (this.readPosition >= this.sourceCode.length) { this.ch = 0 } else { this.ch = this.sourceCode[this.readPosition] } this.poistion = this.readPosition this.readPosition++ } skipWhiteSpaceAndNewLine() { /* 忽略空格 */ while (this.ch === ' ' || this.ch === '\t' || this.ch === '\n') { if (this.ch === '\t' || this.ch === '\n') { this.lineCount++; } this.readChar() } } .... }
readChar() 从代码字符串中逐个读取字符,每读取一个字符,让readPosition加一,每次读取时,代码总是从readPoisition指向的位置开始读取。skipWhiteSpaceAndNewLine函数的作用是,判断读取的字符是不是空格,如果是空格,那么就忽略当前读取的字符,继续读取后续字符,如果字符是回车换行,那么把表示当前行号的变量lineCount加1,然后继续往后读取,直到读取到不是空格,或是回车换行字符为止。当读取到有效字符之后,我们要根据字符的含义把它归类,例如当读取到的字符是’;’时,就创建一个类型为SEMICOLON的Token对象,具体代码实现如下:
class MonkeyLexer { .... nextToken () { var tok this.skipWhiteSpaceAndNewLine() var lineCount = this.lineCount switch (this.ch) { case '=': tok = new Token(this.EQUAL_SIGN, "=", lineCount) break case ';': tok = new Token(this.SEMICOLON, ";", lineCount) break; case '+': tok = new Token(this.PLUS_SIGN, "+", lineCount) break; case 0: tok = new Token(this.EOF, "", lineCount) break; default: var res = this.readIdentifier() if (res !== false) { tok = new Token(this.IDENTIFIER, res, lineCount) } else { res = this.readNumber() if (res !== false) { tok = new Token(this.INTEGER, res, lineCount) } } if (res === false) { tok = undefined } } this.readChar() return tok } .... }
nextToken函数在执行时,先通过调用skipWhiteSpaceAndNewLine,滤掉空格以及回车换行等特殊字符,一旦独到有效字符后,进入switch部分,如果当前的字符是特殊字符,例如’;’,’=’,’+’等,由于这些字符各自属于单独一个分类,因此分别给他们创建里一个Token对象,如果读到的是普通英文字符或者是数字字符,那么就进入default代表的代码处。
当代码连续读入的字符是普通英文字符或是数字字符时,词法解析器会把这些字符凑成一个字符串,假设读入的代码是:
five = 123;
那么解析器读入上面语句时,首先它会连续读入5个字符: f, i, v, e,然后把他们组合成一个字符串”five”,接着为该字符串生成一个分类为IDENTIFIER的Token对象,当解析器读入’=’后面的内容时,它会把后面的数字字符分别读入,也就是分别读取’1’,’2’,’3’三个字符,然后把这三个字符组合成字符串”123”,最后给这个字符串创建一个类型为INTEGER的Token对象。这些工作分别由函数readIdentifier() 和 函数 readNumber()来实现,我们看看他们的代码:
isLetter(ch) { return ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') || (ch === '_') } readIdentifier() { var identifier = "" while (this.isLetter(this.ch)) { identifier += this.ch this.readChar() } if (identifier.length > 0) { return identifier } else { return false } } isDigit(ch) { return '0' <= ch && ch <= '9' } readNumber() { var number = "" while (this.isDigit(this.ch)) { number += this.ch this.readChar() } if (number.length > 0) { return number } else { return false } }
readIdentifier 在执行时,先调用isLetter来判断当前读入的字符是否是字母,如果是,那么它就把所有字符集合起来,形成一个字符串。readNumber在执行时,它先判断当前读入的字符是否是数字,如果是,它就把所有数字字符集合起来,形成一个数字组成的字符串。在nextToken的switch语句部分,如果逻辑进入default部分,那么函数会调用readIdentifier()看看当前是否读到了一个由字母组合成的字符串,如果是,那么就创建一个类型为IDENTIFIER的Token对象,如果不是由字母组成的字符串,那么就接着调用readNumber看看当前内容是不是全是由数字组成的字符串,如果是,那么就创建一个类型为INTEGER的Token对象,如果不是,那说明当前读到了词法解析器无法理解的字符,因此返回一个undefined对象。
更详细的讲解和代码调试演示过程,请点击链接
到目前为止,我们的词法解析部分已经基本成型了,现在就看如何调用起MonkeyLexer这个组件,以便用来分析在页面文本框中输入的代码。要想运行MonkeyLexer这个组件,我们需要把页面文本框中的内容得到,然后传入到该组件中。
回到MonkeyCompilerIDE.js文件,页面加载时,该文件里的MonkeyCompilerIDE.render 函数会被调用,以便用于渲染页面。render在执行时返回了一个JSX对象,其中有一个控件是这样的:
<bootstrap.FormControl componentClass = "textarea" style={textAreaStyle} placeholder="Enter your code" />
上面这个控件的作用就是在页面上创建出一个输入文本框。当用户在文本框上输入内容后,点击下面的红色按钮,我们如何得到框内的文本内容呢?要想实现这个功能,我们必须要获得控件的实例对象,把上面的控件代码做如下修改:
<bootstrap.FormControl componentClass = "textarea" style={textAreaStyle} inputRef = { (ref) => {this._textAreaControl = ref} } placeholder="Enter your code" />
注意看,我们增加了部分代码如下:
inputRef = { (ref) => {this._textAreaControl = ref} }
inputRef是Reactjs给我们提供的指令,如果一个控件,如果它要想在页面上绘制或是创建内容的话,它必须实现一个render()接口,render()接口会被reactjs框架调用,于是组件就可以在render中去绘制页面,那么render()是如何被reactjs调用的呢?当一个组件被放入到”<>”,这两个尖括号中时,reactjs解析到后就会自动把尖括号里面的组件对象得到,然后调用它的reander函数。例如上面代码中,夹在尖括号中的组件叫bootstrap.FormControl, 那么reactjs在解析到上面代码时,会自动调用bootstrap.FormControl.render(),于是一个输入文本框就会显示到页面上了。
如果要想把尖括号包围起来的组件对象获取到,就得依靠inputRef指令,就像我们上面做的那样,当reactjs解读尖括号中的组件时,如果发现其中包含inputRef指令,那么他就会执行后面大括号里面的代码,上面代码中,ref变量就是reactjs框架传给我们的组件对象,其中this指向的是MonkeyCompilerIDE这个组件对象本身,this._textAreaControl = ref 它的意识是,在MonkeyCompilerIDE这个对象内部创建一个名为_textAreaControl的成员变量,然后把ref指向的控件对象赋值给它,这样我们就可以获得文本框控件的实例对象,有了实例对象,我们通过访问它的value属性就可以获得文本框内的文本了。
接下来我们需要关注的是如何响应底层按钮的点击。在JSX中,对应按钮的组件是:
<bootstrap.Button bsStyle="danger"> Lexing </bootstrap.Button>
上面的代码经过reactjs解析后会在页面上绘制出底部那个红色的按钮,其中bsStyle=”danger” 称之为组件的属性,是用来从将信息从外部传入组件内部的,后面我们会详细讲解这个特性。如何响应按钮的点击时间呢?如下:
<bootstrap.Button onClick={this.onLexingClick.bind(this)} bsStyle="danger"> Lexing </bootstrap.Button>
我们增加对onClick事件的捕捉,一旦用户点击按钮后,onClick事件被触发,它会调用我们自己实现的onLexingClick函数,这里一定要使用bind把onLexingClick绑定,要不然被调用时,this指针不指向MonkeyCompilerIDE组件。我们再看看响应函数的实现:
onLexingClick () { this.lexer = new MonkeyLexer(this._textAreaControl.value) this.lexer.lexing() }
我们先通过new 构建一个MonkeyLexer实例,this._textAreaControl.value对应文本框中输入的代码内容,并把创建的实例赋值给当前组件的lexer成员变量,最后调用MonkeyLexer导出的lexing函数开始词法解析流程。
上面代码完成后,加载页面,在文本框中输入几句代码,点击按钮进行词法解析,结果如下:
我在左边输出了两条语句:
let five = 5; let six = 6;
右边控制台输出了词法解析的结果,其中变量”five”形成的Token对象中,分类为1,对应我们的代码,它就是IDENTIFIER, 第二行的数字6,它对应的Token中,分类值为4,对应到代码中是NUMBER,并且它所在的行号是1,从这两处结果看,词法解析的结果基本正确。但有个问题就是let, 它对应的Token中分类是1,对应的就是IDENTIFIER, 这是有问题的,前面我们说过,let是关键字,它必须对应自己的分类,因此词法解析在这里出了点问题,下一节,我们再处理它。
更详细的讲解和代码调试演示过程,请点击链接
如果点击后视频还没有,那表明视频还在云课堂的审查过程中,敬请期待。
更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号:
相关文章推荐
- Reactjs开发自制编程语言Monkey的编译器:语法解析
- Reactjs开发自制编程语言Monkey的编译器:高能技术干货之语法高亮1
- Reactjs开发自制编程语言Monkey的编译器:高能技术干货之语法高亮2
- Reactjs开发自制编程语言Monkey的编译器:使用组件的state机制实现屏幕取词
- 设计自制编程语言Monkey编译器:使用普拉特解析法解析复杂的算术表达式
- reactjs自制Monkey语言编译器:解析组合表达式,ifelse语句块和间套函数调用
- 开发自制语言Monkey编译器:实现复杂算术表达式的执行
- 杯具啊,混合语言编程的弊端出现了,兼谈js的开发工具
- java开发编译器:C语言逻辑控制语句if else if 的语法解析
- 自制monkey语言编译器:符号系统与代码执行
- 对于初学者学习Java语言的建议-Java基础-Java-编程开发
- 【非常强大】 对各种语言的简单解析,带你走向编程之路
- JS 语言的Function 解析
- iOS网络编程开发—JSON解析与XML解析
- 5种语言混合编程:C++、JS、python、Lisp、汇编
- android 开发之webview解析html,js数据交互
- 专访Rust——由Mozilla开发的系统编程语言(目标人群就是那些纠结的C++程序员,甚至也是他们自己)
- Learning Web Development with Bootstrap and AngularJS.pdf( Bootstrap 和 AngularJS 协同开发电子书免费下载)
- 初识reactJS的组件化开发(一):简单封装
- 面向UI编程:ui.js 1.0 粗糙版本发布,分布式开发+容器化+组件化+配置化框架,从无到有的艰难创造