前端小学习
Front-End
I. Intros
某天我回过头来看自己大一写的游戏,越玩越觉得制作尽量细节拉满(确实,不过估计是因为我大一非常闲,可以整天泡在写游戏里)。虽然如此,我还是觉得Pygame不适合做这个游戏,并且我觉得大一时的代码设计思想还不成熟,非常乱,想重构这个游戏。思来想去,用Unity(写了个弹珠打砖块游戏)觉得不爽,并且C#语言风格与C++类似,不想重复,遂想用一些(感觉上)完全不一样的语言去做这件事,最后确定用前端写网页游戏。前端说有趣,也还挺有趣的(毕竟我之前一直想当建筑设计师,搞设计的热情还是有的),但总感觉少了点深度思考(可能因为我接触的太简单)。为了在实践中学习前端,我将之前用Pygame实现的用户登录界面用JS升级了一下(只是功能升级,并没有更好看,见Github:Enigmatisms/JSen),本文记录在做这个小小项目过程中遇到的一些问题。
Ethians Alpha 1.0 主菜单 | 一个(个人认为的)人性化的登录/注册网页 |
II. HTML碎片知识
众所周知,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 | <input type="text" value="" placeholder="Username"/> |
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>也是换行的块级元素)。我把它用在了用户名或密码输入不符合要求时的提示信息显示上:
<div>元素是一个容器,非常常用,常见于网页的组织(相同功能的放一起,可以认为就是花括号了)。它是块级元素(这意味着它通常都是另起一行,并且结束后会换行的),所以不引入一些魔法(比如CSS组织)可能没办法让其不换行显示(可能只是我不知道而已):
III. CSS碎片知识
我从来没有系统学过CSS(或者是HMTL,都是要用的时候学一点算一点)。虽然如此,我觉得其中有些内容还是有必要搞清楚的,毕竟也不能只会而不知道为什么。
3.1 定位
css每个元素可以指定position
,我接触过的只有其中三个(准确来说是四个,第四个static
没有显式用过):
relative
:relative会保持正常的文档flow,比如写如下的代码:
1 | span.relative { |
根据以上的CSS代码在body中放置两个同级的span:
1 | <span class="relative">This div element has position: relative;</span> |
结果是这样的:
说明文档flow(元素的先后位置关系)没有被破坏。
absolute
则是绝对定位:它会将元素从文档flow中取出,比如将上面span.text
的position属性改为absolute
会得到这样的结果:
本来span.test
这个inline元素应该跟随在span.relative
的后面,但是由于test
从flow中单独提取出来了,它相对于其最邻近的relative父级元素(本例子中就是<body>)定位。
注意,如果需要使用absolute定位,其定位方式是相对于 最邻近的relative 父级元素。
3.2 显示
display
可以有这样四种常用的选项:none
, inline-block
,inline
, block
none
:元素不被渲染,不占空间。不像visibility: hidden
一样,hidden
的元素虽然看不见,但是也占位置inline-block
:- 与
inline
不同之处在于:它可以设置行内元素的width height以及margin(相当于一个不添加换行符的小block) - 与
block
不同之处在于:已经说了,它不会换行显示
- 与
3.3 子元素选择
这里只简单记录使用到的一些语法(我其实也并没有仔细去学的打算,前端学习工作的优先级很低)
#
可以直接指定id,比如#first
将会指定id = first
元素的样式.
是class selector,可以不指定元素:比如下代码块第一行,也可以指定元素:比如第二行
1 | .classname {...} /* 表示所有 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是不允许的(本质是加载本地文件),如果直接在本地使用文件打开,会报如下的错误:
但是如果我使用服务器,将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 | #Use to create local host |
实际上代码并不是我写的,代码是我从两个Stackoverflow回答中整理出来的(非常抱歉,这两个回答我也不记得来源了,太久远了)。其中第一个回答只是python3的http request server(stackoverflow上有人回答了用python2设置的本地http服务器,下面就有个哥们把他代码改成python3了)。然而我发现初版代码有个很大的问题:这tm打开服务器就关不掉了,我记得当时作者调用了个这玩意:
1 | httpd.serve_forever() |
Ctrl + C
根本关不掉,而平台又是windows,没办法Ctrl + Z
再kill %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?