前端小学习

Front-End


I. Intros

​ 某天我回过头来看自己大一写的游戏,越玩越觉得制作尽量细节拉满(确实,不过估计是因为我大一非常闲,可以整天泡在写游戏里)。虽然如此,我还是觉得Pygame不适合做这个游戏,并且我觉得大一时的代码设计思想还不成熟,非常乱,想重构这个游戏。思来想去,用Unity(写了个弹珠打砖块游戏)觉得不爽,并且C#语言风格与C++类似,不想重复,遂想用一些(感觉上)完全不一样的语言去做这件事,最后确定用前端写网页游戏。前端说有趣,也还挺有趣的(毕竟我之前一直想当建筑设计师,搞设计的热情还是有的),但总感觉少了点深度思考(可能因为我接触的太简单)。为了在实践中学习前端,我将之前用Pygame实现的用户登录界面用JS升级了一下(只是功能升级,并没有更好看,见Github:Enigmatisms/JSen),本文记录在做这个小小项目过程中遇到的一些问题。

Ethians Alpha 1.0 主菜单 一个(个人认为的)人性化的登录/注册网页
Figure 1. 目标 与 现阶段 发展不平衡之间的矛盾

II. HTML碎片知识

Figure 2. href="html是编程语言!!!.com"

​ 众所周知,HTML是编程语言(误)。作为一种标记语言,当然要去记其中的标记以及js中如何调用这些元素。这里只举一些简单的例子:

2.1 控件元素

​ html5中有一些很有趣的"控件",比如button (按键),input 文本输入框,file文件上传框等等。这些元素有共性,也有特殊的用法。比如button和input都可以 focus 以及 blur

  • focus:聚焦。对于input来说,focus函数会使得文本输入框像是被选中了一样(如果网页中存在focus之后的text输入框,那么键盘输入会直接出现在这个输入框中)。button的focus... 可能就是单纯的定位吧(使得网页翻页到button所在位置)。举个例子:可以写一个这样的功能,使得用户输入用户名后按enter键可以直接跳转到密码输入框上 ---> 密码输入框.focus()
  • blur:focus的反义。举个例子:用户填写信息之后希望放弃本次填写,第一次ESC使得输入框不再被选中,第二次直接返回上一级。

​ 对应的,有一些事件驱动的函数,比如onkeyup(用户按键抬起时的行为),onclick(点击时的行为),onblur(用户取消选中时的行为)。当然这很多都是和JS相关的。

​ 不同之处比如:

1
2
<input type="text" value="" placeholder="Username"/>
<input type="password" value="" placeholder="Password"/>

​ text可以指定自己的类型,指定为password可以自动回显成:black_circle:,好,很有精神。

2.2 name / class / id

​ 怎么说呢,只能简单区分一下,因为我还没有遇到这三个的坑(触及本质的那种)。

  • name:重名是完全可以的,一个html中可以有多个name属性为同一个值的元素。可以在js中使用:getElementsByName,注意element用了复数形式,返回的是一个NodeList(可以下标索引)。name方便了同种类型元素的类似操作。比如我写的那个 自动评教脚本
  • id: 这玩意貌似是每个html文件唯一的,毕竟js方法是:getElementById:唯一表示了一个元素的存在(特化元素的好方法)。
  • class: 为什么要用class呢?感觉name处理了重复性,id处理了唯一性,class好像没事干。同一class可以有不同的name,同一name也可以有不同的class,class属性目前我在css中遇到过,css不方便定义一个name的样式,但是可以定义一个id的样式,而如果需要多元素统一样式,可以使用class。

2.3 span & div

​ <span>是内联元素,内部可以填充文本。内联元素的好处就是:我不换行显示。这样可以创建一些有name/id/class的文本而不换行(注意<p>也是换行的块级元素)。我把它用在了用户名或密码输入不符合要求时的提示信息显示上:

Figure 3. span的应用(红色字体只会在输入不符要求时出现)

​ <div>元素是一个容器,非常常用,常见于网页的组织(相同功能的放一起,可以认为就是花括号了)。它是块级元素(这意味着它通常都是另起一行,并且结束后会换行的),所以不引入一些魔法(比如CSS组织)可能没办法让其不换行显示(可能只是我不知道而已):

Figure 3. 很多div

III. CSS碎片知识

​ 我从来没有系统学过CSS(或者是HMTL,都是要用的时候学一点算一点)。虽然如此,我觉得其中有些内容还是有必要搞清楚的,毕竟也不能只会而不知道为什么。

Figure 4. Game of Front-end Thrones

3.1 定位

​ css每个元素可以指定position,我接触过的只有其中三个(准确来说是四个,第四个static没有显式用过):

  • relative:relative会保持正常的文档flow,比如写如下的代码:
1
2
3
4
5
6
7
8
9
10
span.relative {
position: relative;
left: 0px;
border: 3px solid blue;
}
span.test {
position: relative;
left: 0px;
border: 3px solid blue;
}

​ 根据以上的CSS代码在body中放置两个同级的span:

1
2
<span class="relative">This div element has position: relative;</span>
<span class="test">This div element has position: relative;</span>

​ 结果是这样的:

Figure 5. 双relative

​ 说明文档flow(元素的先后位置关系)没有被破坏。

  • absolute则是绝对定位:它会将元素从文档flow中取出,比如将上面span.text的position属性改为absolute会得到这样的结果:

​ 本来span.test这个inline元素应该跟随在span.relative的后面,但是由于test从flow中单独提取出来了,它相对于其最邻近的relative父级元素(本例子中就是<body>)定位。

​ 注意,如果需要使用absolute定位,其定位方式是相对于 最邻近的relative 父级元素

3.2 显示

display可以有这样四种常用的选项:none, inline-blockinline, block

  • none:元素不被渲染,不占空间。不像visibility: hidden一样,hidden的元素虽然看不见,但是也占位置
  • inline-block:
    • inline不同之处在于:它可以设置行内元素的width height以及margin(相当于一个不添加换行符的小block)
    • block不同之处在于:已经说了,它不会换行显示

3.3 子元素选择

​ 这里只简单记录使用到的一些语法(我其实也并没有仔细去学的打算,前端学习工作的优先级很低)

  • # 可以直接指定id,比如#first 将会指定id = first元素的样式
  • .是class selector,可以不指定元素:比如下代码块第一行,也可以指定元素:比如第二行
1
2
.classname {...}		/* 表示所有 class = "classname"的元素*/
span.classname {...} /* 表示 span的class = "classname"的元素*/

​ 有些有趣的例子可以看这篇文章说的: Difference between "#header .class" & "#header.class"

IV. JS碎片知识

4.1 浏览器端js与服务器端js

​ 本人还并没有开始Node.js的学习,只是了解了以下浏览器端JS的写法。开始时我还不知道这两者有什么区别,直到遇上了这么一个问题:

  • Log 5.0希望可以从本地加载用户数据,以便sign in时可以比对用户。
  • Sign up时可以写出到本地文件

​ 查了好久,都没有发现有什么接口可以帮我读出或者写入到本地文件的。最后面向Google,有人说:浏览器上运行的JS出于安全性考虑,是不允许写入和读取本地数据的,如果需要实现文件操作,最好使用一个服务器host的文件。实际上这就不是很前端了。

​ 而Node.js 本质上是后端语言(只不过可以使用JS这个前端语言编辑),Node.js常用于服务器端,它没有:

  • BOM(Browser Object Model):对浏览器进行访问和操作的模型。也就是说,window这个没有了
  • DOM (Document Object Model): document以及其下属元素也没有了

​ 原来本人上手用浏览器端js,但想做一些后端的事情。

学习语言这样的东西时,我延续了Python/C++的学习方式:上手就是自行设计项目并通过实践来学习。这种学习方式有的时候可能并不好,特别是在其前驱知识不牢固的情况下。不看理论、教程、文档可能只能让我们明白 如何解决问题 而不是 如何分析问题并设计方法

4.2 本地服务器小坑

​ 我们已知浏览器出于安全考虑,不能随便加载本地文件这一事实。加载本地文件不一定要是进行数据的读入或者写出,比如最基本的 跨模块调(引)用 都属于一种加载本地文件的行为。比如我有两个文件:a.js 以及b.js,其中:

  • a.js相当于一个utility模块,定义了很多有趣的常用函数
  • b.js相当于一个客户模块,需要使用a.js的函数
  • c.html调用了b.js模块(作为网页的行为)

​ 那么这涉及到import与export。但是由于基于本地文件的import, export是不允许的(本质是加载本地文件),如果直接在本地使用文件打开,会报如下的错误:

Figure x. CORS错误 无法加载特定的本地文件

​ 但是如果我使用服务器,将c.html定义的网站挂在上面,再使用服务器对应的url访问网页时并不会有文件访问限制。这实际上涉及到两个协议以及一个policy:

​ 双击html文件可以直接打开为网页,此处使用的是file protocol,打开网页时浏览器url显示:

1
file:///<path to your file>

​ 使用服务器也可以加载网页,此时使用http(s)协议,按照个人的理解,一次http请求访问大概是这样的流程:

  • 域名解析:用户输入url,url经过DNS服务转为IP返回给用户
  • 三次握手:由于http(是应用层的)在传输层上使用TCP/IP协议,故需要握手建立连接
  • 网站服务器通过http协议回传html、css、javascript等文件到本地
  • 本地浏览器解析html等文件,渲染网页

​ 虽然乍一看,两种方式并没有本质上的区别,都是通过某种方式获得网页文件,在本地进行渲染,那么对于文件访问这种事情,本来不应该有区别的。但本地访问却受到如上图所说的 CORS policy限制:

Cross-Origin Resource Sharing (CORS) is an HTTP-header based mechanism that allows a server to indicate any origins (domain, scheme, or port) other than its own from which a browser should permit loading resources.[1]

​ 与之相对的一个概念叫做:Same-origin policy,关于same origin policy,这篇文章讲得很清楚: MDN Web Docs: Same Origin Policy. 这个policy的大概意思是说,【协议(比如同http或者同https)】【端口】【host】三者必须相同。而本地打开的文件,并没有host,也没办法做到host相同。并且有人这么说:

Chrome doesn't believe that there's any common relationship between any two local files.[2]

​ 除了在浏览器中直接disable此安全设置,否则没办法直接绕开(事实上,我觉得绕开也非常不优雅)。假如不绕开,那就只有一个选择了,使用服务器host我们的网页。于是我写了一个http server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#Use to create local host
import http.server
import socketserver
PORT = 8080
class NoCacheHTTPRequestHandler(
http.server.SimpleHTTPRequestHandler
):
def send_response_only(self, code, message=None):
super().send_response_only(code, message)
self.send_header('Cache-Control', 'no-store, must-revalidate')
self.send_header('Expires', '0')

if __name__ == '__main__':
NoCacheHTTPRequestHandler.extensions_map.update({".js": "application/javascript",})
httpd = socketserver.TCPServer(("", PORT), NoCacheHTTPRequestHandler)
httpd.timeout = 1
while True:
try:
httpd.handle_request()
except KeyboardInterrupt:
httpd.server_close()
break

​ 实际上代码并不是我写的,代码是我从两个Stackoverflow回答中整理出来的(非常抱歉,这两个回答我也不记得来源了,太久远了)。其中第一个回答只是python3的http request server(stackoverflow上有人回答了用python2设置的本地http服务器,下面就有个哥们把他代码改成python3了)。然而我发现初版代码有个很大的问题:这tm打开服务器就关不掉了,我记得当时作者调用了个这玩意:

1
httpd.serve_forever()

Ctrl + C根本关不掉,而平台又是windows,没办法Ctrl + Zkill %1,非常烦。于是我google(如何才能让http server不阻塞呢?(因为我发现server每次Ctrl + C没有反应是因为阻塞在一个奇怪的循环里)),最后找到别人写的NoCacheHTTPRequestHandler,再设置一下timeout就可以很方便关闭了。

​ 为什么要关闭呢?这里有个我没明白原理的坑:每次我不关闭server,或者没有完全关闭(可能只是挂起了),在修改js代码后刷新网页或者重新访问是不会更新行为的。我怀疑是它端口一直开着,使用的是cache过的网页,故js代码更新并不会引起网页行为的更新。

4.3 export & import

​ 我一开始以为export以及import的使用就会像我每天早上起床一样简单(确实很简单),但实际上export与import只在解决完http服务器问题之后才开始用(本地file协议根本是不允许的,两个js文件origin都是null,没办法互相访问,除非在html中使用丑陋的全局变量)。

​ 但是export与import我遇到了default 以及non-default (name imports) 问题。

​ 首先,import export必须要在 module中使用。module与一般的js文件不同,在引入html时,需要定义(type=):

1
<script type="module" src="xxx.js"></script>

​ 比如在我的练手小项目里:common.js定义了一些多个js文件可以共用的函数,其中一个js文件如signin_modules.js调用了common.js中的一些函数或类,html文件直接加载的是signin_modules.js,那么signin_modules.js就必须是一个module。我开始觉得很疑惑,为什么signin_modules.js是module?不应该是common.js是一个 模块 才对么?实际上,signin_modules.js(使用import的js)是top level module,其他的被引用文件都是底层module。import处定义了module,被引用的文件会自动变为lower level modules。如果不加type=module将会报错:

SyntaxError: import declarations may only appear at top level of a module.

​ 其次,是named以及default的区别。

​ named import因为带有变量(或者自定义类型)名,故可以同时调入/调出多个元素。但要记住,named一定需要使用花括号包住!

1
import {bar, fool} from "module name"

​ 不管是引入一个还是多个,都需要使用花括号包住。

​ default import/export 的好处就是不需要提供名字,但这也导致了每个module只能有一个default import/export。default情况下不要使用花括号。当然,也可以把default写出来:

1
export default some_value;			// default可有可无,但花括号一定无

​ 搞错了default/named的结果(并且还不知道这个机制),就是会找不到模块中的错误(之前搞错了一直以为模块中定义有问题)。


Reference

[1] MDN Web Docs: Cross-Origin Resource Sharing (CORS)

[2] Code Redirect: "Origin null is not allowed by Access-Control-Allow-Origin" in Chrome. Why?