Service workers essentially act as proxy servers that sit between webapplications, the browser, and the network (when available). They areintended, among other things, to enable the creation of effectiveoffline experiences, intercept network requests and take appropriateaction based on whether the network is available, and update assetsresiding on the server. They will also allow access to pushnotifications and background sync APIs.
Service worker 在 web应用、浏览器和网络之间扮演代理服务器的角色。可以用于创建有效的离线体验,劫持网络请求等。
在这篇文章中我创建了一个示例项目来展示 service worker的基本用法及如何缓存图片。可以在
首先使用 Vite 来创建一个新项目,运行 yarn create vite
后选择 vanilla 即可。
在 public
目录下创建一个名为 sw.js
的service worker 文件:
1 | // sw.js |
开启开发模式 yarn dev
后,可以在类似http://localhost:5173/sw.js
的位置看到这个文件。
Service woker 在生效前要经历注册(register)的过程,在main.js
文件中加入以下代码来注册 service worker:
1 | if ('serviceWorker' in navigator) { |
启动项目并打开页面,会在控制台看到以下内容:
第一条输出来自 sw.js
文件,表明这个 service worker已经运行。第二条输出来自 main.js
文件中对 service worker注册的过程,表明成功注册了。
打开 Application 标签页可以看到 service worker 状态是activated and is running:
如果这时候刷新页面,会发现控制台中少了一条输出:
因为现在是已注册的状态了,所以这个文件不会再次运行。
如果你点击 Unregister 按钮,则会取消这个 service worker文件的注册。之后刷新页面就会重新经历注册流程。
有一点需要注意的是 serivce worker文件的位置决定了它的作用域,比如上面代码中的 sw.js
可以控制整个 domain 的 fetch
事件。假如 service worker文件在 /example/sw.js
,那么它只能看见 URL 以/example/
开头的页面(例如 /example/page1/
,/example/page2/
)上的 fetch
事件。
我们已经知道了如何让一个极简的 service worker文件正常运行,接下来看看如何进行图片的缓存。
先在 html 中加入几张图片,任意放几张图片文件放到 public
目录下:
1 | <div id="app"> |
此时在 Network 中查看图片请求,Size栏展示的是图片的大小。当然如果没有勾选 Disable cache的情况下,这里也可能显示 (memory cache)
或者(disk cache)
。
接下来修改 service worker 文件 sw.js
,加入以下内容:
1 | const cacheName = 'cache-name-v1'; |
看一下这段代码的效果,为了能让更新后的 service worker文件生效,需要重新打开页面。现在 Size 栏展示的是(ServiceWorker)
,表示图片已由 service worker进行了缓存。
接下来解释下这段代码做了什么。
我们一直在说 service worker可以用来缓存图片,这个缓存是放在哪里呢?这就先需要了解 Request / Respone
对象的持久化存储机制。你也可以在 service worker以外来使用它,打开控制台:
1 | const cacheName = 'cache-name-v1'; |
这里就是先打开了 caches storage,然后依次对缓存内容进行操作。
1 | self.addEventListener('fetch', async (event) => { |
这里监听了网络请求事件,在事件处理函数中我们就可以处理请求和响应了。先判断出这是一个图片请求:event.request.destination === 'image'
,然后用event.respondWith()
来返回响应内容。
下面这一段包含在 event.respondWith()
之内,是在决定要要返回什么。先在缓存中检查请求是否已有响应缓存,如果已经有了就直接返回,否则就执行fetch 并将这次的响应放入缓存中。
1 | caches.open(cacheName).then((cache) => { |
上面看到在 Network 中查看图片请求都显示来自 service worker,可以从Application -> Cache Storage 里查看具体的缓存内容。
你可能会问用 service worker 缓存有什么好处?浏览器中不是已经有了memory cache 和 disk cache 了吗?
一个使用场景是离线化应用,尝试对 sw.js
文件做一点小改动,注释掉这个对类型的判断:
1 | if (event.request.destination === 'image') { |
重新打开这个页面后会发现所有的资源都会走 service worker了,这时即使你断开网络也是可以正常展示的。
利用 service worker也可以允许开发人员更精细地控制哪些资源被缓存,以及在何时更新缓存。
既然我们利用 service worker可以劫持所有图片的请求,那么还可以做些其他的优化,比如将图片修改为更加合适的格式。我们知道webp 是一种比 png更新的图片格式,一般会有更小的体积。某些图片服务可能提供这样的功能,在这样的url上(http://www.example.com/1.png
)添加一个后缀就(`http://www.example.com/1.png?format=webp
)得到了对应格式的图片。
尝试下实现这个功能,先给用到的图片生成对应的 webp格式,放在同一位置,比如 image1.png 生成 image1.webp。
定义一个函数来做 url 的替换,仅将后缀做个替换:
1 | function getNewUrl(url) { |
然后改写 fetch 事件的监听函数:
1 | const cacheName = 'cache-name-v2'; |
注意这里我们将 cacheName
修改为了cache-name-v2
,因为之前版本的 service worker 已经将一份image1.png
的缓存放在了名为 cache-name-v1
的缓存中,代码检查到图片缓存请求已存在也就不会发出新的请求。新建一个cache key 来保存新的缓存。
看下改动的效果:
可以看到虽然图片名称仍然是 image1.png
,但是 Content-Type已经改变了。这里 webp 格式几乎比 png格式小了一半体积,当然这主要是因为我的 png图片没有经过压缩处理。真实的场景中,由 png 格式转换成 webp不会有这么大的体积优化。
关于 service worker 还有一点需要了解,因为 service worker可以任意修改网络请求,所以处于安全原因需要在 HTTPS 环境中才能启用。
这是 CSAPP Lab 的
Lab 正常运行和测试需要安装相应的环境,利用 docker可以省去配置环境的环节,我使用的这个已经制作好的 docker 镜像:
Docker 启动命令如下:
1 | docker run -p 7777:7777 -v "$PWD/labs:/home/csapp/project" xieguochao/csapp |
然后在浏览器中打开 http://localhost:7777
即可。
-v
参数是将本地的 labs
目录挂载到 docker容器中,容器内部的目录路径是 /home/csapp/project
是容器内部的目录路径,这样在容器中修改的文件也会同步到本地。如果想再挂载一个目录,比如建立了一个和labs
目录同级的 learn
目录,用于放置一些测试代码,可以再添加一个 -v
参数:
1 | docker run -p 7777:7777 -v "$PWD/labs:/home/csapp/labs" -v "$PWD/learn:/home/csapp/learn" xieguochao/csapp |
Data Lab 要完成的目标可以查看它的文档:
简要来说:这个 lab是考察你对各种位运算和数据表示的理解,bits.c
中给出了一些函数,需要你在给定的限制下将函数实现补充完整。
改动 bits.c
文件后,通过以下命令来测试:
1 | make |
使用 ./dlc bits.c
检查是否有不允许使用的运算符。
虽然我作为一个前端开发,在工作中直接使用位运算的情况并不多,但是这个lab 拿来练习并补充知识盲区是不错的。
1 | /* |
说明:
异或运算 ^
结果为真就是 x
和 y
中有且有一个为真,x ^ y = (x & ~y) | (~x & y)
。因为题目要求不允许使用|
,所以需要使用德摩根定律转换一下,将表达式中的|
转换成 &
。
1 | /* |
说明:
最小的补码数,就是最高位为 1,其余位为 0 的数,将 1 左移 31位即可。
1 | /* |
说明:
最大的补码数,就是最高位为 0,其余位为 1 的数。
它与上一题的 tmin
所有位均不同,如果将两个数异或,得到的应该是一个全 1 的位表示。
1 | !~((1 << 31) ^ x); |
但是这一题中我们不能使用移位,所以需要换一个思路。
对于 tmax
可以计算 x + 1 + x
得到一个全 1的数,取反后得到全 0 的数。但是有一个特殊情况,就是 x = -1
的时候(即所有的位均为 1),x + 1 + x
会溢出,取反后它也会得到全 0 的表示,还需要排除这种情况。
1 | /* |
说明:
A 的二进制表示为 1010,易得 0xAAAAAAAA
的所有奇数位都是1。如果 x
的所有奇数位都是1,将 x
与0xAAAAAAAA
进行与操作后,得到的结果应该是0xAAAAAAAA
。
1 | int mask = 0xAAAAAAAA; |
因为不允许使用 ==
来判断相等,可以改用异或来判断是否相等,一个数和它本身的异或结果为0。
1 | int mask = 0xAAAAAAAA; |
改动之后仍然存在一个问题,我们允许定义的常量最大为0xFF
,所以不能直接使用0xAAAAAAAA
,可以定义常量0xAA
,通过移位来判断每8位是否都符合要求。
1 | int y = x & (x >> 8) & (x >> 16) & (x >> 24); |
1 | /* |
说明:
~x + x
一定是全 1 的表示,+1
后会溢出得到0,所以x + (~x + 1) == 0
。可以得出补码数的相反数,就是取反加一。
1 | /* |
说明:
这道题的思路是找到数字范围对应的上下界。
数字中最大的值是0x39
,它会有对应的一个上界,0x39 + 上界
得到最大的正数(即除符号位外均为 1)。假如 x > 0x39
,则x + 上界
会溢出得到一个负数。 可以利用 x
与上界相加后的符号位来判断。
数字中最小的值是0x30
,它会有对应的一个下界,0x30 + 下界
得到0。假如 x < 0x30
,则 x + 下界
会得到一个负数。 可以利用 x
与下界相加后的符号位来判断。
以 4 位数字来举例说明,能表示的最大的正数是
1 | /* |
说明:
返回的结果是 y 和 z 中的一个,表达式应该是这样的形式(y op expr) | (z op expr)
我们需要这样的一个掩码值(表格中以16位为例):
x | mask | y & ~mask | z & mask |
---|---|---|---|
0 | 0xffff | 0x0000 | z |
非0 | 0x0000 | y | 0x0000 |
这个掩码值可以通过表达式 ~!x + 1
根据 x的不同情况来求出:
x | !x | ~!x | ~!x + 1 |
---|---|---|---|
0 | 0x0001 | 0xfffe | 0xffff |
非0 | 0x0000 | 0xffff | 0x0000 |
1 | /* |
说明:
比较大小需要区分 x
和 y
的符号是否相同。符号相同,比较差值;符号不同,需要满足x < 0
。
为什么要区分符号相同和符号不同的情况呢?假设是 8位二进制表示的数字,补码表示的范围是 [-128, 127]。x
和y
的值如下:
1 | x = 0x80; // -128 |
y - x = 255
,这个值在 8 位二进制补码表示的是 -1,但是y > x
。所以需要区分符号相同和符号不同的情况。
当二者符号相同时,x <= y
即y - x >= 0
,y - x
使用~x + 1 + y
来计算,如果符号位是 0 则表示满足。
1 | /* |
说明:
除了 0 以外,一个数和它的相反数一定是一正一负,因此利用x
和 -x
的符号位可以得到结果。
1 | int howManyBits(int x) { |
说明:
对于正数,符号位为0,关键在于找到最高位的1的位置,然后加上一个符号位。
对于负数,最高位一定是1,而最高位的若干个连续的1可以等价于1个单独的1(符号扩展)。
比如 \(1110_2 = -8 + 4 + 2 =-2\),它和 \(10_2 = -2\)是相等的。
因此,关键在于找到最高位的零的位置,如果我们将负数各位取反,则思路和正数一致了,都是需要确定最高的1 的位置。
假设对于最大的正整数:2^31-1
1 | 0111 1111, 1111 1111, 1111 1111, 1111 1111 |
假设对于2
1 | 0000 0000, 0000 0000, 0000 0000, 0000 0010 |
1 | /* |
说明:
根据浮点数的表示依次处理符号位、指数部分、小数部分即可。
具体说明可以参考这一篇:
1 | /* |
说明:
在浮点数表示中,指数部分就代表了是 2 的多少次幂,将 x加上偏置值得到指数部分,然后左移至正确位置即可。
我在完成这个 lab的时候参考了别人的实现方案,以下是一些给了我帮助的文章:
这些年我换过许多笔记软件,最开始使用的是Evernote/印象笔记,因为可以实现电脑端和手机端的同步,加上方便的浏览器的剪藏扩展,成了我大学时期的主力应用。
后来我买了一台微软的 Surface3 平板,附赠一年的Office365,其中就包含了 OneNote 应用。OneNote笔记本的设置深得我心,搭配手写笔的体验很好,它的搜索功甚至能直接搜到图片上的文字。
虽然 OneNote 有着强大的格式功能,但是不原生支持 markdown格式,笔记中的代码格式一直是个问题。之后我切换到了 Mac 平台,接触到了Bear(熊掌记),第一次使用的感觉很惊艳,支持markdown,内置了多款主题可供选择。为了获得设备间的同步功能,我选择了付费。Bear也成了我这几年最常用的笔记应用。
Bear最大的优点就是它的颜值高,能在文件任意位置添加标签也很方便。但是它也有一些缺点:只能在苹果平台使用;没有目录功能;不支持表格。
这几年间,Bear 几乎没有什么功能上的更新,多年前开发者就声称在开发的Web 编辑器也遥遥无期。
在这期间,我又试用过了Notion,功能的确强大,但当时使用的版本有几个缺陷:当时对免费用户能使用的 Block数量有所限制;搜索功能有欠缺;用着卡顿;需要翻墙。因为 Notion的强大功能,甚至会有专门针对 Notion的教程,让我开始思考我真的需要这么多功能吗。
我还面临一个问题,既然可以随手添加新笔记,当有了几百条没有整理过的笔记之后,查看起来也不方便,我的解决方案是换一个新的笔记应用,最后笔记分散在了多个不互通的应用中。
市场上有足够多的笔记应用了,包括收费的以及各种开源的。假如我需要功能1、功能2,但是可能应用A具有了功能1,而只有应用B有功能2,难道需要把两个应用都安装上。
我放弃了寻找一个完美的笔记应用,决定自己写一个笔记文件的管理应用,暂时将编辑功能放在一边,先专注于文件的管理。
我的笔记大部分是计算机相关的,有着大量的代码片段,markdown格式是必选的。我很喜欢 Bear 中添加标签的方式,只要标签是以#
开头即可,而市面上大部分给 markdown文件加标签的方式是使用 yml
配置。
我理了下我的需求: 1. 支持 markdown,尽量是标准的 markdown 1.支持文件夹管理 1. 支持标签管理(#tag
的格式) 1.文件保存在本地,与应用分离 1. 可以用 git 来做版本的控制 1. 主题可自定义1. 可多端同步 1. ...
开发中我选择使用一个本地开启的 Node.js服务器来处理文件,在浏览器中进行 UI的展示,两者间通过接口通信。经过了一段时间的开发后,完成了我的笔记管理应用
在完成了基础功能之后,我就在平时开始了使用。这个应用专注于文件的管理,需要编辑功能时则由应用提供的快捷功能切换到VSCode 或者 Typora中完成。作为唯一用户的我也觉得不方便,考虑到这一点,基础的编辑功能不可少。
如果需要融合编辑器的功能,整个应用可能需要利用 electron技术来进行重构。在开始这么做之前,我问了自己一个问题:我开发的这个应用的编辑体验能超过Typora 吗?我估计了一下自己能投入的时间精力,得出的结论是不可能。
于是我换了一种思路,我对于 markdown 文件的编辑工作是用 VSCode 和Typora 完成的,既然如此,何不写一个 VSCode 的扩展来完成呢。
借用 VSCode扩展的方式,我将不需要考虑文件的编辑、本地图片展示、文件中相对路径等等问题。在学习了VSCode的扩展开发之后,我将 knowledge-center 工具改造成了扩展版KaiNotes。它提供了按标签将文件分类的功能。从下面可以获取使用地址和代码地址,或者直接在VSCode 中搜索 kainotes 安装使用。
扩展现在已经完成了基础功能的开发,自我感觉使用体验不错。
现在我的笔记流程是这样的: 1. 创建一个 git 仓库用于存放笔记文件 - Git自带了文件的编辑历史 - 只要将仓库同步至远端就相当于做了备份 2.编辑文件时使用 VSCode 或 Typora - 简单的编辑在 VSCode中在完成,突出一个方便快捷,预览功能可搭配 Markdown Preview Enhanced扩展 - 较复杂的编辑会选择Typora,所见即所得搭配可选择的主题会有很好的编辑体验 3.笔记文件的管理会在 VSCode 中使用我的 kainotes 扩展来完成 -按标签查看相关的笔记 - 利用标签词云来直观查看笔记数量 4.需要记录零散内容则使用备忘录或Bear -备忘录足够方便,多种设备均可输入,iPad 还可使用手写 - Bear 同步功能和markdown 输入不错,既然给 Bear 续了费,那就继续使用下去, -零散的内容根据需要整理到笔记仓库中
KaiNotes还有不少功能等着开发,比如重命名标签、收藏文件等功能,还会继续完善。
最近在更新了 Mac 系统后,发现苹果的备忘录应用也支持添加#tag
格式的标签了。如果备忘录能支持markdown,它就几乎能满足我的所有需求了。
现在还有双链笔记的概念,roam、logseq等应用我还没有尝试过。以后可能会取其所长整合到我的工具中去,或者更新我的笔记管理方式。
笔记工具太多了,在使用它们的过程中我一直在提醒自己不要本末倒置,工具是次要的,内容才重要。记下来的笔记也是需要复习整理的,才能从单纯的笔记转变为知识。
]]>现在考虑这样一种情况,如果想分享的代码来自于一个 repo中的文件,博客中需要引用其中的代码。比如在我的上一篇文章
基于此,我们可以采用这样的一种思路,通过某种方式自动将 repo中的文件同步至 gist,然后在博客中引用 gist,从而保证更新了 repo中的文件后,博客中的代码也会自动更新。
这里的某种方式就是用 Github Actions来实现的。Github Action提供了自动运行的工作流,可以在某些时机触发然后执行相应任务。我们要实现的功能可以在我们每次push 代码的时候触发,然后同步 gist。
Github的市场提供了很多官方及第三方的 action,可以从中挑选合适的 action来实现需求,以下的两个 action 都可以用于实现我们的目的:
以 actions-deploy-gist
为例,需要这么几步:
ghp_3vDXRBAzjXA5rtDMyWMofSNeLwxL4W3*****
的 accesstoken,会在下一步用到。GIST_TOKEN
(名字可随意,只要与后面的配置文件保持一致),值为上面的token。1 | - uses: actions/checkout@v2 |
完整的配置可参考我在这个项目中的
有趣的一点,注意到上面的这个 gist 引用中的内容就是 action的配置,而这个 gist本身就是靠其中的内容实现了同步,形成了一个“自指”。
因为我需要同步多个文件至一个 gist 中,所以我重复了uses: exuanbo/actions-deploy-gist@v1.1.2
多次。(这里可能会有更佳的写法,待更新)
完成以上工作后,每次更新 repo,相应的 gist 就会改变了。
接下来我们看下如何在博客中引用 gist:
1 | <!-- 使用 script 标签是通用的方法 --> |
这里有一个小问题,如果一个 gist中包含有多个文件,上面的方式会将所有的文件都展示出来,如果想只展示其中一个文件,可以加上文件名,如下:
1 | <!-- 在 url 上用 file 参数带上文件名 --> |
如果文件名中有空格:
1 | <!-- 使用 %20 来代替空格,注意 encodeURIComponent(' ') == '%20' --> |
经过上面的操作后,就可以在博客中引用 repo 中的文件了,更新 repo后,博客中的文件也会保持同步。
]]>Node.js 中提供了这些有用的 API:
fs.readdir
:异步读取文件夹fs.readdirSync
:同步读取文件夹fs.statSync
:同步获取文件属性对于遍历的结果,我们可以选择按列表或文件树来展示。先从最简单的情况看起,用同步方式处理,返回结果是一个列表。
先使用 fs.readdirSync
获取文件列表,然后遍历文件列表,使用 fs.statSync
获取列表中文件的状态,如果是文件,则添加到文件列表中,如果是文件夹,则递归调用traverseFolderList
函数,直到获取到所有文件。
如果我们想展示文件夹目录结构,那么列表格式的就不太方便了。假设有如下的文件夹结构:
1 | ./1 |
希望获取到的对象结构如下:
1 | { |
这个对象以文件/文件夹相对于根目录的相对路径为key,每个节点包含了这些属性:
type
:用于区分文件或文件夹类型path
:相对路径children
:如果是文件夹类型,则其中是子文件的相对路径在上面的实现中,都是使用了同步的方式来处理,即fs.readdirSync
方法,可以使用异步方式来处理吗?
可以选择 fs.readdir
来异步读取文件夹,但是回调函数的调用方式不太方便。在 Node 10+ 中提供了fs.promises
API,其中提供了一些文件系统的方法,它们返回的是一个 Promise对象,而非使用回调函数。这里可以从 fs.promises
中引入readdir
方法,从而可以使用方便的 async/await
语法来进行异步处理,避免了回调函数的方式。
1 | const { readdir } = require('fs').promises; |
将上面的 traverseFolderList
方法重写为异步格式:
traverseFolderList
和asyncTraverseFolderList
返回的结果都是列表格式,我们可以写一个测试脚本来比较下二者的运行时间:
分别用两个函数遍历了同一个文件夹十次后,统计结果如下,异步方式比同步方式减少了约18%的时间。
1 | 同步 - 平均耗时:1217.1ms |
注意一点,本文中的代码都是没有做错误处理的,实际上读取文件时可能会出错,因此将相应的代码使用try...catch
包起来是一个合理的做法。
]]>https://stackoverflow.com/a/45130990
fs.promisesAPI https://javascript.info/promisify
Peter Norvig 的这篇文章
这篇文章中记录了我的理解而非原文的翻译,如果需要更详情的解释,可以查看原文。
首先需要考虑的是如何表示数据,数独的行使用 A-I 的字母表示,列使用 1-9的数字表示。代码中用到了以下的术语/变量:
square
所有81个格子的标记['A1', 'A2', 'A3', ... , 'I7', 'I8', 'I9']
unit
一个格子所在的行、列或九宫格unitList
包含9行、9列、9个九宫格,共27个 unitpeers
格子所在三个 unit 中的其他格子,共20个grid
使用文本格式来表示数独的初始状态,1~9代表数字,0或.代表此处未填入values
一个以 square 为 key 的map,给出每个格子的可能值,eg.{'A1':'12349', 'A2':'8', ...}
对于数独的初始状态使用文本格式来表示,如下所示的三种格式代表的是同一个数独:
1 | 4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4...... |
这部分的 JS 代码实现可以看代码:
开始时,每个格子都可以是 1~9的任何数字,然后从初始状态开始给每个格子填入相应的数字。解数独的过程就是在减少每个格子可以填入的数字,直到所有格子都能且只能填入1个数字。这里需要用到两种方法:约束传播(ConstraintPropagation)和搜索(Search)。
在处理数独的初始状态时用了 parseGrid()
函数,其中调用了assign(values, square, digit)
方法,即将 digit 填入square。
解数独有两个重要策略:
这里需要使用 assign()
和 eliminate()
两个函数,代码见:
经过这个过程后一些简单的数独就可以得出解了,但对复杂的数独并非如此。所以需要使用搜索(Search)来进一步处理。
这里的搜索指的是系统地尝试所有的可能性,直到找到解。对于数独来说就是对于每个未确定的格子,尝试填入一个可能的数字,然后继续搜索。如果出现了矛盾,就换一个数字。这是一个递归的过程,即深度优先搜索(depth-firstsearch)。
search()
函数的具体代码见:
整体的流程图如下:
flowchart TB %% init(初始化)开始 --> gridValues("gridValues()\n将字符串表示转换为map");gridValues --> assign("assign()"\n给特定格子分配一个数字);assign --> canAssign{分配过程\n没有矛盾?};canAssign -- 无法分配 ----> 无解canAssign-- 可以分配 --> eliminate("eliminate()\n进行约束传播");eliminate --> onlyOne{发现某个格子只有\n唯一的可能性}onlyOne -- 是的 --> assignonlyOne -- 否 --> search("search()\n依次尝试可能的数字\nDFS递归处理");search --> found{每个格子都能\n分配唯一的数字?}found -- 不行 --> 无解 --> 结束found -- 可以 --> 找到答案 --> 结束
至此就完成了数独的解法。
]]>最近需要实现这么一个功能,通过拖动来改变窗口左侧的文件列表栏的宽度。我选择了用styled-components
来实现这个功能,并写了如下的样式组件,其中将边栏当前的宽度width
作为属性传入。
1 | const StyledFileList = styled.div<{ width: number }>` |
但是当我快速拖动边栏时,width
变化得非常频繁,在控制台中展示了如下的 Warning。
大意是仅为这一个组件就生成了超过 200 个class。利用开发者工具的查找元素,改变 width
时可以观察到元素上的 class 名称一直在变化。
1 | <div width="270" class="sc-bdvvtL ckAtiw"></div> |
可以确定 styled-components
给每一个不同的width
属性都生成了一个 class。如果在 html 中搜索相应的class 名称(如上面的代码中是 ckAtiw
和bBcytR
),可以在 <head>
的<style>
标签中找到它们对应的样式内容,稍加比较就可以发现除了 width
以外的部分都是相同的。
styled-components
处理样式的过程是这样的:属性改变 ->重新生成样式 -> 插入到 <head>
中,这个过程如果重复很多次,会产生大量的 CSS内容冗余,很有可能造成性能问题。而我们可以通过使用 CSS变量来解决这个问题。
我是在这篇文章 styled-components
中使用 CSS 变量(CSSvariable)的技巧。
CSS 变量是以 --
开头的属性名称,它们的值可以是任何类型的值,使用var(--your-variable-name)
来应用样式。
1 | --your-awesome-color: darkcyan; |
这一行文字就是应用了上面的 CSS,你可以在控制台中修改样式并观察变化。
对于之前的边栏组件,我们可以换一个写法来解决,不再使用props
来传入宽度,而是使用 CSS 变量,组件代码如下:
1 | <StyledFileList |
此时再改变左侧边栏的宽度,可发现元素的 class 名称是固定的,只有 style在变化。
1 | <div class="sc-bdvvtL jGeOyv" style="--width:210px;"></div> |
如果检查 <head>
中的内容,会看到在<style>
标签中相应的样式内容也只出现了一次,从而解决了样式冗余的问题。
]]>
Thestyled-components Happy Path Demystifyingstyled-components
- Generate screenshots and PDFs of pages.
- Crawl a SPA (Single-Page Application) and generate pre-renderedcontent (i.e. "SSR" (Server-Side Rendering)).
- Automate form submission, UI testing, keyboard input, etc. ...
几乎所有能手动在 Chrome 进行的操作现在都可以用 Puppeteer 来完成。
我之前在工作中就用到了 Puppeteer做了一些页面爬取的工作,这里把使用到或者玩过的一些功能做了下总结和记录。这篇文章并不是一个全面的Puppeteer 的使用教程,毕竟 Puppeteer 的大部分 API 我也没有使用过🙂。
Puppeteer 的使用就是如同一个普通的 npm package 一样,使用npm init
新建一个项目后,使用npm install puppeteer
即可。如果安装过程中卡在了下载Chromium 的过程中,可以考虑使用 puppeteer-core
来代替,具体可见
先由最基础的打开页面功能开始,新建一个 index.js
文件,写入下方的代码。
1 | const puppeteer = require('puppeteer'); |
现在运行node index.js
,如果成功运行,你将会看到什么也没有发生😕。这是因为,Puppeteer默认是在 headless
模式下运行的,为了能看到确实打开了页面,可以在打开浏览器时加入参数{ headless: false }
。
1 | const puppeteer = require('puppeteer'); |
一个典型的流程如下:打开浏览器 -> 打开新页面 -> 去往指定页面-> 关闭页面(可省略) -> 关闭浏览器。
运行上面的代码,可能会出现 TimeoutError
的出错提示,这是因为访问指定页面的速度较慢导致了超时。你可以将pageUrl
换成任意国内地址重试。
默认情况下 Puppeteer 的超时时间为 30s,如果在 30s之内没能打开页面,会有 timeout 的出错提示。 1
UnhandledPromiseRejectionWarning: TimeoutError: Navigation timeout of 30000 ms exceeded
可以通过如下设置解决:
1 | const page = await browser.newPage(); |
Puppeteer 提供了 screenshot
API,可以将页面截图保存下来。运行下面的代码将会在 js文件所在的路径下保存一张名为 screenshot.png
的图片文件。这里有一点需要注意,在之前的代码中,我们没有设置窗口的大小,Puppeteer默认打开的是 800*600
尺寸的窗口,截图的大小也为800*600
(即使是在 headless
模式下也是如此)。可以通过 setViewport
来改变窗口的大小。
1 | const path = require('path'); |
同样也可以将页面保存成一个 PDF 文件。
1 | const path = require('path'); |
注意一点,保存为 PDF 的时候,必须在 headless
模式下进行。
平时我们浏览的网站可能针对不同的设备会有不同的展示效果,区分移动端和PC端、Android或 iOS 等。Puppeteer 就提供了模拟设备的功能。
可以使用 page.emulate(options)
来模拟设备,可以在这个文件中看到支持的设备列表:
1 | const puppeteer = require('puppeteer'); |
也可以添加自己的 UserAgent的方式来模拟列表中没有的设备。下面的代码中以 iPad Pro 为例,说明如何设置UserAgent(其实 iPad Pro是存在于上面的设备列表中的,这里只是举了个例子)。
1 | const puppeteer = require('puppeteer'); |
考虑一下这样的场景,如何获取到页面上的所有的图片。当然我们可以拿到整个页面,然后使用document.querySelector()
筛选出所有的 img
标签。这样会有一点小问题,如果图片是以 CSS 的background-image
形式引入的,查找 img
标签是无法找到的。如果图片地址是由 js动态改变的,可能也会缺失部分图片。
另一种可选的方式是通过监听 response
的方式来处理。可以认为在 Chrome 开发工具的 Network Tab 下能看到的每一个response 都可以在这里拿到。
1 | const path = require('path'); |
通过 response.headers
可以获取响应的 header信息。如果有需要也可将响应保存成本地的文件。
未完待续
]]>19年仍然看了大约二十本的新书,数量上和以前差不多。
小说类: - Flowers for Algernon - 鞑靼人沙漠 - 上帝的图书馆 -日本合众国 - 了不起的盖茨比 - 挪威的森林 - 环界(1-4)
非虚构类: - 程序员修炼之道 - 黑客与画家 - 影响力 - 未来简史 - 经度 -阅读是一座随身携带的避难所 - 那些科学家们彻夜忧虑的问题 - 量子物理史话 -怪诞行为学1:可预测的非理性
19年初买了任天堂的 Switch,于是从 Steam 那里省下的金钱和时间又花在了Switch 的游戏上。下面每个游戏都花了十几小时到上百小时不等。
下半年买了《三十天学会绘画》,没能像书名里写的那样在三十天里看完,用了大约半年终于在12月看完了这本书。按照书里的教程画了一遍之后,很明显,还没有学会绘画。于是又买了本《素描的诀窍》,准备在2020年继续画下去。
虽然我知道自己定了新年计划,也是不会去完成的。但希望自己在2020年多看点计算机类的书籍,多写点博客做点记录。2019年的年度总结没赶上元旦完成,今天就祝所有人春节平安快乐吧。
]]>我将代码开源在了
Draft.js 是 Facebook 推出的用于 React的富文本编辑器框架,初始化一个最基本的 Draft.js 的代码如下:
1 | import React from ‘react’; |
可以Editor
传入一个 editorState
属性,并绑定一个onChange
事件,当发生编辑操作时,返回一个新的editorState
。这样我们就得到了一个可以进行基本的文本操作的编辑器了。
在说明什么是 EditorState
及 Draft.js对于数据的存储方式之前,需要简略介绍一下Immutables.js。
Draft.js 是利用
在 Draft.js 的使用过程中,可能会遇到下面这些数据结构。
Map
类似于 js 中的对象,用.set()
和 .get()
方法进行写和读。
1 | const Immutable = require(‘immutable’); |
OrderedMap
混合了 object
和 array
的特点。通过使用orderedMap.get(‘key’)
和orderedMap.set(‘key’, newValue)
这两个方法,可以将它当成一个普通的 object
来使用。但和Map
的不同点在于其中的 key 是按照被加入时的顺序排序的。
Record
也类似于Map
,但有一些不同之处。 - 一个 record
一旦被初始化,就不能再添加新的 key 了 - 你可以给一个 record
实例添加默认值
还有一点,immutable 的对象,提供了toJS()
方法,可将其转成普通的 js对象,这一方法在想查看其内部内容时非常有用。
Immutable.js 参考文章:
ImmutableData with Immutable.js | Jscrambler Blog
在创建基本的编辑器的时候,我们用到了 EditorState
。EditorState
是编辑器最顶层的状态对象,它是一个 ImmutableRecord对象,保存了编辑器中全部的状态信息,包括文本状态、选中状态等。
调用 editorState.toJS()
可将 immutable record转换成一个普通的 object,打印出来如下:
简单地来看下其中的部分内容: - currentContent
是一个ContentState
对象,存放的是当前编辑器中的内容 -selection
中是当前选中的状态 - redoStack
和undoStack
就是撤销/重做栈,它是一个数组,存放的是ContentState
类型的编辑器状态 - decorator
会寻找特定的模式,并用特定的组件渲染出来
既然编辑器中的内容是存储在一个 ContentState
对象中,那么这个 ContentState
又是什么?
ContentState
也是一个 Immutable Record对象,其中保存了编辑器里的全部内容和渲染前后的两个选中状态。可以通过EditorState.getCurrentContent()
来获取当前的ContentState
,同样调用 .toJS()
后将它打印出来看下:
blockMap
和 entityMap
里放置的就是编辑器中的 block
和entity
,它们是构建 Draft 编辑器的砖瓦。
一个 ContentBlock
表示一个编辑器内容中的一个独立的block,即视觉上独立的一块。
以下图的编辑器作为一个例子,图中的四个红框标出的部分都是block。在平时阅读文章时,内容是以段落为单位的,在编辑器中每个段落就是一个block,如第一个和最后一个红框中的文字内容。第二个红框中是一张图片,它也是一个block,但显示方式不同于普通的block,为了自定义它的显示方式还需要额外做一些工作,后面会加以详细说明。
还有一点需要稍作说明,第三个红框中虽然是空白,但它也是一个block,只不过其中的文本为空而已。
此时,输出一下 convertToRaw(currentContent)
,看看其中的内容。注意这里的输出结构与上面的currentContent.toJS()
略有所区别,这里只有blocks
和 entityMap
这两项。 blocks
这个数组中依次存放了各个 block
的信息,每一个 block
都是一个 contentBlock
对象。
每个 contentBlock
都有如下的几个属性值: -key
: 标识出这是哪一个 block - type
:这是何种类型的 block - text
: 其中的文字 - ……
Draft.js 中 block
的 type
有unstyled,paragraph,header-one,atomic …… 等值,在 Draft.js 的文档中atomic
类型对应的是 <figure />
元素,我们也选取了它来实现插入图片的功能。
图中的这些 block 的除了第三个 key = “1u22q” 的 block 的 type 值是atomic
外,其余的值都是“unstyled”
。再仔细看下这个 atomic
类型的block:
除了 key
,text
,type
等值之外,在 entityRanges
这一项中保存它保存了使用到的entity
的信息:offset 和 length 确定了 entity
在 block 中的范围,而 key 则能让我们去取出对应的entity
。
回到上面的打印出的 contentState
的内容,除了blocks
数组外还有一个 entityMap
对象。它是以entity
的 key
作为键值的对象,里面保存了图片、链接等种类的 entity
信息,从中就可获得 blocks
所需要的entity
。
1 | entityMap: { |
以上介绍了 Draft.js是如何对编辑器中的数据进行存储的,接下来会从代码实现的角度来说明插入图片是如何实现的。
插入图片有着这样的流程:首先为图片创建一个entity
,然后创建一个带有这个 entity
的新EditorState
,然后更新即可。以下是关键部分的代码:
1 | import { AtomicBlockUtils } from 'draft.js'; |
上面我们已经见到了,一张图片是作为一个 atomic
类型的block 插入的。Draft.js 提供了blockRendererFn
让我们可以自定义 ContentBlock
的渲染方式,给它传入一个函数后,由该函数来判断这个 block 的type
是什么,然后决定如何渲染。
以下的这段代码来自 atomic
的ContentBlock
。
1 | function myBlockRenderer(contentBlock) { |
可以看到这里传递了一个 props
:
1 | component: MediaComponent, |
结果等同于<MediaComponent foo='bar' />
,可以利用这里的props
传入所需要的其他数据。
这里我们就可以定义一个自己的 MediaComponent
来决定展现方式。因为不管是图片还是视频等其它的媒体类型,它们的type
都是 atomic
。在MediaComponent
里就需要通过 entity
的type
来确定其种类。
1 | const entity = props.contentState.getEntity(props.block.getEntityAt(0)); |
当 entity
的 type
是我们自定义的image
时就可以返回 <Image />
组件了。
1 | <Image src={src} /> // 自定义的图片组件 <Image /> |
既然已经插入了图片,那么如何删除它呢?当然我们可以按键盘上的Backspace 键来删除。也可以在图片的右上角加入一个 “X”的图标,点击后删除该图片,实现方式如下。
1 | deleteImage = (block) => { |
至此,对图片的相关操作就完成了。
在本文中,介绍了 Draft.js 的基本功能,它是如何进行数据的存储的,及EditorState
、ContentState
、ContentBlock
、Entity
等对象间的关系。并以此为基础说明了如何在编辑器中对图片进行操作。
当然关于 Draft.js还有很多内容没有在本文中提及,如修改行内文本的样式,利用decorators
来插入与渲染链接等等。这些就需要读者探索下Draft.js 的官方文档和其他人的分享并亲自尝试下了。
本文所基于的编辑器项目:draft-editor
]]>
HowDraft.js Represents Rich Text Data Buildinga Rich Text Editor with React and Draft.js, Part 2.4: EmbeddingImages Draft.js在知乎的实践
今年做的最重要的事应该是换了份工作吧,也换了座城市,4月份从南京来到了上海。
来到了更大的公司,见识了不同的工作流程,比起以前只有几个开发的小公司正规了不少。工作比以前更忙了些,也尝试了一些新的技术,大部分还是浅尝辄止。
今年在一个工作项目中第一次使用了 React Native开发,了解了RN的一点皮毛,体会了下多端融合开发。
今年的开发中碰到了不少移动端的兼容性问题,要考虑页面在各种手机、APP、各种版本的webview 中打开的情况,有了一些零散的经验。
似乎没有正经地看什么技术书籍。
看的新书没有多少,倒是在空闲时又翻了翻以前看过的书作为消遣。
看了两本科普的小册子,还有《费曼物理学讲义》只看了十章
哦,还有把45卷的《哆啦A梦》漫画作为睡前读物看完了(上一次看哆啦A梦漫画的时候是十几年前名字还叫做机器猫小叮当)。
今年可以算是没怎么运动过吧,一共跑了20次,102.5公里。如果给自己找个借口,就是周围的环境太差,没有跑步的想法。还是承认自己太懒算了。
去电影院看了10部电影,4部国产,6部国外。仍然看了很多连名字也记不起的影视剧。
反思2018年,应该是花了太多的时间在网上看各种段子和无聊图吧,导致脑子里装满了各种烂梗,挤占了留给技术的地方╮(╯_╰)╭。
2019年应该多花点时间在看书上,包括技术书和其他类型的书。
再见,2018。
]]>我觉得要解释 minimax算法的原理,需要用示意图来解释更清晰,以下的几篇文章都对原理说的足够清楚。
2048-AI程序算法分析 TicTac Toe: Understanding the Minimax Algorithm AnExhaustive Explanation of Minimax, a Staple AI Algorithm
其中后面的两篇文章都是以 tic-tac-toe 游戏为例,并用 Ruby 实现。
以棋类游戏为例来说明 minimax算法,每一个棋盘的状态都会对应一个分数。双方将会轮流下棋。轮到我方下子时,我会选择分数最高的状态;而对方会选择对我最不利的状态。可以这么认为,每次我都需要从对手给我选择的最差(min)局面中选出最好(max)的一个,这就是这个算法名称minimax 的意义。
(图片来自于http://web.cs.ucla.edu/~rosen/161/notes/alphabeta.html)
我们接下来会解决这样一个问题,如上图所示,正方形的节点对应于我的决策,圆形的节点是对手的决策。双方轮流选择一个分支,我的目标是让最后选出的数字尽可能大,对方的目标是让这个数字尽可能小。
为了简单起见,对于这个特定的问题,我用了一个嵌套的数组来表示状态树。
1 | const dataTree = [ |
图中的节点分为两种类型:
先定义一个 Node
类,constructor
如下:
1 | constructor(data, type, depth) { |
根节点的 depth
为0,以下的每一层 depth
依次加一。最底层的节点 depth
为4,其 data
是写在图中的数字,其它层节点的 data
均是一个数组。
接下来考虑如何给每个节点打分,可能会出现这样的几种情况:
为方便描述,我们按照由上到下、由左到右的顺序给图中节点进行标号。节点1是max节点,从节点2和节点3中选择较大值;而对于节点2来说,需要从节点4,5中选取较小值。很显然,我们这里要用递归的方法来实现,当搜索到最底层的节点时,递归过程开始返回。
以下是打分函数 score
的具体代码:
1 | score() { |
完整的minimax 算法代码
Alpha-beta 剪枝算法可以认为是 minimax算法的一种改进,在实际的问题中,需要搜索的状态数量将会非常庞大,利用alpha-beta 剪枝算法可以去除一些不必要的搜索。
关于 alpha-beta 算法的具体解释可以看这篇文章
剪枝算法中主要有这么些概念:
每一个节点都会由 alpha 和 beta 两个值来确定一个范围 [alpha,beta],alpha 值代表的是下界,beta代表的是上界。每搜索一个子节点,都会按规则对范围进行修正。
Max 节点可以修改 alpha 值,min 节点修改 beta 值。
如果出现了 beta <= alpha的情况,则不用搜索更多的子树了,未搜索的这部分子树将被忽略,这个操作就被称作剪枝(pruning)。
接下来我会尽量说明为什么剪枝这个操作是合理的,省略了一部分节点为什么不会对结果产生影响。用原图中以4号节点(第三层的第一个节点)为根节点的子树来举例,方便描述这里将他们用A - G 的字母来重新标记。
从 B 节点看起,B 是 min 节点,需要在 D 和 E 中寻找较小值,因此 B取值为3,同时 B 的 beta 值也设置为 3。假设 B还有更多值大于3的子节点,但因为已经出现了 D 这个最小值,所以不会对 B产生影响,即这里的 beta = 3 确定了一个上界。
A 是 max 节点,需要在 B 和 C 中找到较大值,因为子树 B 已经搜索完毕,B的值确定为 3,所以 A 的值至少为 3,这样确定了 A 的下界 alpha = 3。在搜索C 子树之前,我们希望 C 的值大于3,这样才会对 A 的下界alpha 产生影响。于是 C 从 A 这里获得了下界 alpha = 3 这个限制条件。
C 是 min 节点,要从 F 和 G 里找出较小值。F 的值为2,所以 C的值一定小于等于 2,更新 C 的上界 beta = 2。此时 C 的 alpha = 3, beta =2,这是一个空区间,也就是说即使继续考虑 C 的其它子节点,也不可能让 C 的值大于 3,所以我们不必再考虑 G 节点。G节点就是被剪枝的节点。
重复这样的过程,会有更多的节点因为剪枝操作被忽略,从而对 minimax算法进行了优化。
接下来讨论如何修改前面实现的 minimax 算法,使其变为 alpha-beta剪枝算法。
第一步在 constructor 中加入两个新属性,alpha、beta。
1 | constructor(data, type, depth, alpha, beta) { |
然后每次都搜索会视情况更新 alpha, beta 的值,以下的代码片段来自于搜索max 节点的过程:
1 | // alphabeta.js 中的 score() 函数 |
相对应的是在 min 节点中,我们更新的将是 beta值。好了,只需要做这么些简单的改变,就将 minimax 算法改变成了 alpha-beta剪枝算法了。
最后看看如何将算法应用到 tic-tac-toe 游戏中。
完整的alpha-beta 剪枝算法代码
Tic-tac-toe,即井字棋游戏,规则是在双方轮流在 3x3的棋盘上的任意位置下子,率先将三子连成一线的一方获胜。
这就是一个非常适合用 minimax来解决的问题,即使在不考虑对称的情况,所有的游戏状态也只有 9! = 362880种,相比于其它棋类游戏天文数字般的状态数量已经很少了,因而很适合作为算法的示例。
我在代码中将棋盘的状态用一个长度为9的数组来表示,然后利用 canvas绘制出一个简易的棋盘,下子的过程就是修改数组的对应位置然后重绘画面。
现在我们已经有了现成的 minimax 和 alpha-beta剪枝算法,只要加上一点儿细节就能完成这个游戏了😀。
先来定义一个 GameState
类,其中保存了游戏的状态,对应于之前分析过程中的节点,其constructor
如下:
1 | constructor(board, player, depth, alpha, beta) { |
为进行游戏,首先需要一个 checkFinish
函数,检查游戏是否结束,结束时返回胜利者信息。搜索的过程是在getScore
函数中完成的,每次搜索先检查游戏是否结束,平局返回零分,我们的算法是站在AI 的角度来考虑的,因此 AI 胜利时返回10分,AI 失利时返回-10分。
1 | // alphabeta.js 中的 getScore() 方法 |
接着是对 max 和 min 节点的分类处理:
1 | // alphabeta.js 中的 getScore() 方法 |
完整代码见
这样就简单地介绍了 minimax 算法和 alpha-beta算法,并分别给出了一个简单的实现,然后在 tic-tac-toe游戏中应用了算法。
文章中所提到的所有代码可见此项目:algorithms
文件夹中是两种算法的简单实现,src
文件中是游戏的代码。
文章开头说到了这篇文章起源于写2048游戏项目的过程中,之后我将 minimax算法应用到了2048游戏的 AI 中,不过对于局面的评估函数尚不完善,现在 AI只能勉强合成1024😢, 还有很大的改进空间。
]]>技术方面,继续写了一年的 React,还试着用了下Vue,学了许多前端库,但是却没有什么完整的项目。
写了一个塔防游戏,待改的 Bug 比待实现的 feature 多,等着 2018继续完善。
在 Coursera 上听了算法课,最后一周的作业没交,所以课程也没完成。
非前端的技术方面,看了本《C程序设计语言》,《深入理解计算机系统》只看到第二章,《Algorithms》也依旧没看完。重新开始学习线性代数了。
前端的书也没看多少,大多数情况下都在网上看资料。记得的书只有《你不知道的JavaScript(上)》和《深入理解 ES6》。
总的来说,技术水平比起2016年有一点长进,但是也没有什么量化的指标,只是JavaScript写的更熟练了。唯一有所记录的是2017年在这里写了7篇博客,无论质量如何,聊胜于无。
读书方面,因为从来不会去数自己读了多少本。只能去翻翻订单记录和在豆瓣上的标记,除去技术类的书籍外,看了以下书籍:-科幻书籍:《盲视》《让时间停止的女孩》《基地系列七部曲》(补上了后面几本)-三本村上春树:《没有色彩的多崎作和他的巡礼之年》《奇鸟形状录》《东京奇谭集》- 三本伊坂幸太郎:《摩登时代》《魔王》《死神的精确度》 -古龙的《小李飞刀:多情剑客无情剑》和《欢乐英雄》 - 苏童的《武则天》 -莫言的《檀香刑》 - 马尔克斯的《一桩事先张扬的凶杀案》 -燕垒生的《天行健》全集 - 罗斯·特里尔的《毛泽东传》 - 杨绛的《我们仨》 -《亮剑》 - 《阿城精选集》 - 《易中天中华史》部分大概就这些书了吧,还有一本哲学书《大问题》尚未翻完。
想看英语原版书来着,不过还没找到什么感兴趣的,于是又去看哈利波特了:- Holes - Hyperion (半本) - Harry Potter and the Sorcerer's Stone -Harry Potter and the Chamber of Secrets - Harry Potter and the Prisonerof Azkaban - Harry Potter and the Goblet of Fire (目前为止看了46%)
17年去电影院看了12部电影,以《降临》为始,以《妖猫传》为终,其它电影不提也罢。至于各种乱七八糟的美剧、英剧、日剧,看了就忘,没有记录。
17年每次跑步几乎都用悦跑圈做了记录,总的次数是43次,一共377公里,差不多一天一公里。
上半年买了日语的教材,又一次停在了五十音图。
下半年买了把吉他,还在练习中,也许应该快要入门了吧。
2017年大概就做了这些事吧。最后,新的一年已经来了,依旧是单身狗,没有变秃,也没有变强。
]]>项目的主要代码在 pell.js
文件中,其结构很简单,主要功能的实现依赖于以下的几个部分
actions
对象exec()
函数init()
函数先从最简单的部分看起, exec()
函数只有下面三行:
1 | export const exec = (command, value = null) => { |
它将 document.execCommand()
进行了一个简单的包装,
1 | bool = document.execCommand(aCommandName, aShowDefaultUI, aValueArgument) |
aCommandName
是表示想执行的命令的字符串,比如:加粗'bold',创建链接 'createLink',改变字体大小 'fontSize' 等等aShowDefaultUI
是否显示默认的用户界面aValueArgument
有些命令需要额外的输入,如插入图片、链接时需要给出地址注:经过我的试验,在 Chrome 下改变 aShowDefaultUI的值并未发现影响,
文件中定义了一个名为 actions
的对象,对应的是下图工具栏上的这一行按钮, actions
中的每个子对象都保存了一个按钮的属性。
部分代码:
1 | const actions = { |
这段代码中显示了名为bold
,italic
,underline
的三个对象属性,对应于工具栏中前方的加粗、斜体、下划线按钮,可以看出它们的结构是相同的,都有下列三个属性:
icon
: 如何在工具栏中显示title
: 就是 title 啦result
:一个函数,会赋给按钮作为点击事件,调用之前所提到的 exec()
函数来对文本进行操作现在已有了 actions
对象,那么如何使用它呢?这就要看看init()
函数了,它会根据一定的规则从 actions
对象中选出元素组成一个数组,数组的每一项都会生成一个按钮。下面代码中的settings.actions
即为此数组,其中的每个元素都对应一个显示在工具栏上的按钮。settings.actions
的生成规则会在后面进行解释。
1 | // pell.js 中的 init() 函数 |
这样数组中的每个元素就都生成了一个工具栏上的按钮了。
想使用 pell 编辑器时,只要调用 init()
函数来初始化一个编辑器即可。它接收一个 setting
对象作为参数,其中包含这样的一些属性:
element
: 编辑器的 DOM 元素styleWithCSS
: 设置为 true 时,将会用<span style="font-weight: bold;"></span>
代替<b></b>
actions
onChange
其中最重要的是actions
,它是一个数组,包含了你想在工具栏显示的按钮列表。
actions
数组中可以有这几种元素: - 一个字符串 - 一个有name
属性的对象 - 一个对象,没有 name
属性,但有生成一个按钮的必需属性 icon
,result
等
1 | actions: [ |
在 init()
函数中会把这个 actions
参数 和pell.js 中定义的 actions
对象组合起来,可以将actions
对象当作一个默认设置,看以下代码:
1 | // pell.js 中的 init() 函数 |
如果参数对象 setting
中不包含 actions
数组,则会默认使用之前定义的 actions
对象来初始化。
init()函数里还有一个重要的部分,就是创建一个可编辑区域,这里创建了一个div
元素,将其 contentEditable
属性设为true
,从而可以在这里使用之前提到的document.execCommand()
命令了。
1 | // 创建编辑区域的元素 |
最后以“插入链接”为例来梳理下整个编辑器的流程:
一、在调用 init()
函数时,在参数对象的action
数组中加入以下一项
1 | { |
二、在 init()
的运行过程中,会检查已定义的actions
对象中是否有 link
这个属性。经检查属性确实存在
1 | link: { |
因为传入的参数中有 result
这一项,所以用传入的result
来代替 link
对象中的默认值,然后将修改过的 link
对象放入settings.actions
数组中。
三、对 settings.actions
数组进行一次迭代来生成工具栏,link
对象作为其中的一项生成了一个“插入链接”的按钮。result
属性成为其点击事件。
四、点击“插入链接”的按钮后,会让你输入一个 url,然后调用exec('createLink', url)
在编辑区域插入该链接。
编辑器其它按钮的功能流程也类似。
这样 Pell编辑器的大部分内容就讲解完毕了,剩余部分还需要自己去看源码。毕竟项目的代码不长,以此作为文本编辑器的入门倒不错。
2017年的最后一篇文章了,再见,2017。
Any application that can be written in JavaScript,will eventually be written in JavaScript. -- Atwood'sLaw
本文来源于我在看了 Milo Yip 在知乎专栏里的这篇文章:
最终结果可见此CodePen:https://codepen.io/noiron/pen/aVgYMB?editors=1010
See the Pen aVgYMBby wu kai (@noiron) onCodePen.
在本文中,我主要解释一下 JavaScript如何将图像输出,以及我对这个画光程序的一点理解。更多有关图形学原理部分的内容,建议还是看Milo Yip 的原文。
Milo Yip 在他的系列文章中使用了一个自己写的 png
格式的图片。而使用JavaScript 可以方便地在 canvas
元素上绘制出图形。
为了能够记录下图片的信息,需要记录每一个像素点的 RGB
值,对于一张宽度为 W,高度为 H 的图片,其像素点数量为W * H
,而每个像素点分别用三个数来表示其 R、G、B值,所以记录下整张图片的数据,需要一个长度为 W * H * 3
的数组。如果图片带有 alpha 通道,需要记录 RGBA
值,则数组长度为W * H * 4
。这里有一个可以简化的地方,因为绘制的是一张黑白的图片,对于黑/白/灰色来说R = G = B,所以用长度 W * H
的数组即可。
假设我们现在已经有了一个记录图片信息的数组p
,那么如何将其显示出来?这里需要用到getImageData
, putImageData
方法。
可以利用 getImageData()
方法来获得 ImageData对象,从中得到图像的像素点。
1 | const ctx = canvas.getContext('2d') |
ImageData
对象的 data
属性是一个数组,包含有每个像素点的 RGBA
,其总长度为W * H * 4
。所以我们将记录图片信息的数组 p
中的值依序赋给 data
,再利用 putImageData
方法即可将图片绘制到 canvas 上了。
1 | function processImageData(imageData, p) { |
现在我们考虑的是单色光,RGB 中的三个值是相等的,当光照越强时,RGB值越大,图像的颜色也越白。
坐标为 (x, y)的一个点,它获得的光来自于各个方向上的光的叠加,即是一个对角度的积分:
\[F\left( x,y\right) =\int ^{2\pi}_{0}L\left( x,y,\theta \right) d\theta\]
其中 \(L\left( x,y,\theta \right)\)代表在二维坐标 (x, y) 在 \(\theta\)方向有多少光经过。
由于无法直接计算出这个积分的值,需要用蒙特卡罗积分法来进行采样。利用N 个方向的采样平均值作为这一点的光强。
那么 (x, y) 点在 \(\theta\)方向上能获得多少光照?我们现在只有一个处于画面中央的圆形光源,可考虑从(x, y) 为起点的一条线段,如果它足够长,那只有两种可能性:
但我们需要对这条线段的长度加以限制,所以逐步加长线段的长度,如果线段终点在光源的表面或内部,则获得光照。当步数达到MAX_STEP
或距离达到MAX_DISTANCE
,停止计算,在此方向上获得的光照为0。
这里需要利用带符号距离场(signed distance field,SDF)来表示出当前的点与场景的最近距离,每次步进此距离能保证不会进入光源的内部。如下图中,每个圆的半径均为圆心和图中形状的最近距离,则按P0 -> P1 -> P2 -> ...的顺序前进能保证不会和图中的形状相交。
(图源:https://developer.nvidia.com/gpugems/GPUGems2/gpugems2_chapter08.html)
此即原文中提到的光线步进(raymarching)方法(又称为球体追踪/spheretracing)。
利用 sample()
函数计算并保存所有坐标点的光照:
1 | const p = []; |
利用蒙特卡罗积分法进行 N 次采样取平均值获得 (x, y)处的光照强度,其中的 trace()
函数代表的是从
1 | function sample(x, y) { |
circleSDF
为带符号距离场(signed distance field,SDF),值为负时,表示在光源的内部。
1 | function circleSDF(x, y, cx, cy, r) { |
最后就是 trace()
方法,用光线步进来计算出 (ox, oy) 沿单位向量 (dx, dy)方向上获得的光照。
1 | function trace(ox, oy, dx, dy) { |
最后我还在代码添加了一个点击事件,可以改变光源位置来查看不同的效果。
[2018-01-01 更新] 在 GitHub上建立了一个项目,准备将《用C语言画光》系列文章中的代码都移植到JavaScipt 中来。 项目地址:
本文将要讨论的是第二个问题 pathfollowing,给定一条路径,看物体如何沿着它从起点运行至终点。为了方便描述,接下来的内容中,用单词Boid来表示行进的物体或塔防中的敌人。
接下来会用一种简单的方法来解决这一问题,最终完成的代码库可见 GitHub:
先来看看如何标识出画面中的位置,首先画面被一系列的横纵线分成了许多网格,对于地图范围内的一个点,它会有自己的像素坐标(x, y),同时它所处的格子也有自己的坐标 (col, row) 或 (xIndex,yIndex),表示所处的列和行。
为了区分,下文中提到像素坐标即为用像素表示的坐标,网格坐标表示点在网格中的列和行。
在这种表示方法下,还需要一个工具函数index2Px(col, row)
,用于计算格子中心的像素坐标。
接下来给出路径的坐标,路径是如下的一个二维数组:
1 | const path = [[0, 1], [COLS - 4, 1], [COLS - 4, 4], [6, 4], [6, 7], /* 部分省略 */] |
每一项都是路径上一个点的网格坐标,将这些点用直线连接起来后就得到了boid 行进的路径。我们的目标就是要让 boid能够从路径第一个坐标移动至最后一个坐标。
先考虑最简单的问题,如何让 Boid 沿着一条直线行进。
物体的移动需要位置和速度,为了表示其像素坐标,boid
需要x
, y
属性;其速度需要 speed
属性,同时还需要一个angle
,以便计算出速度在两个方向上的分量 vx
,vy
。
动画效果的实现需要用 requestAnimationFrame
函数,每一秒为60帧,每一帧中都会执行一次循环,在其中改变位置:
下一时刻的位置 = 当前时刻的位置 + 速度
1 | // 示意代码 |
在每一个循环中,boid 的位置都会发生变化,在新的位置上将其画出即可看到boid 沿直线运动的效果。
这一部分可在示例代码库的 demo01/go-straight
分支上查看:
git checkout demo01/go-straightnpm run demo01
现在 boid已经动起来了,但是却没法停止,这就是我们接下来需要考虑的问题。
要让 boid能够知道自己到达了目标点,则在每一次循环过程中,需要计算出此刻离目标点的距离分量dx
,dy
,据此算出距离dist
,将其与速度 speed
进行比较。如果dist > speed
,说明物体离目标点还挺远,继续将速度加到位置上即可。反之则表明物体将要到达终点,此时若直接加上速度,boid可能会越过目标点,因此需要一点不同的处理。
1 | // 示意代码 |
这一部分可在示例代码库的 demo01/stop
分支上查看:
git checkout demo01/stopnpm run demo01
此时,到达了终点的 boid 被清除而不再显示。
前面叙述中为了简化,路径中只有起点和终点,所以 boid没有机会转向,那当路径变复杂了之后,boid 该如何运动?
前面已经提到过,path
是一个记录了路径网格坐标的数组,boid会从中取一个坐标作为自己的当前目标点,然后一直向前行进,到达了这个目标点之后,它会从path
数组中取出下一个坐标,继续移动至该位置。循环以上过程,直到 boid 到达path
中的最后一个坐标。
上面的代码中,我们的目标点 target
固定为path
的最后一个坐标,而现在每一次转向时 target
都会变化,所以加入这样的两个变量:
waypoint
表示当前目标点的索引angleFlag
记录是否需要转向。1 |
|
这一部分可在示例代码库的 demo01/steering
分支上查看:
git checkout demo01/steeringnpm run demo01
结果可见下图:
到此为止,这种 boid 沿路径行进的方法已经讲解完毕了。建议读者查看一下repo 中的代码,自己修改部分代码,比如更改路径,看结果会有何不同。
这一种方法中的确实现了沿路径移动的效果,但是有点儿单调,boid只能在路径的中轴线上移动,而且它们之间也没有交互的效果。
我之前参考他人的代码实现了
(也许之后会补一篇博客来介绍 The Nature of Code中的实现,但谁知道会不会写呢🤔)
最后,
React Router v4推出已有六个月了,网络上因版本升级带来的哀嚎仿佛就在半年前。我在使用这个版本的React Router时,也遇到了一些问题,比如这里所说的代码分割,所以写了这篇博客作为总结,希望能对他人有所帮助。
在用户浏览我们的网站时,一种方案是一次性地将所有的 JavaScript代码都下载下来,可想而知,代码体积会很可观,同时这些代码中的一部分可能是用户此时并不需要的。另一种方案是按需加载,将JavaScript代码分成多个块(chunk),用户只需下载当前浏览所需的代码即可,用户进入到其它页面或需要渲染其它部分时,才加载更多的代码。这后一种方案中用到的就是所谓的代码分割(codesplitting)了。
当然为了实现代码分割,仍然需要和 webpack 搭配使用,先来看看 webpack的文档中是如何介绍的。
Webpack文档的 code splitting 页面中介绍了三种方法:
entry
配置项来进行手动分割CommonsChunkPlugin
插件来提取重复 chunk你可以读一下此篇文档,从而对 webpack是如何进行代码分割的有个基本的认识。本文后面将提到的方案就是基于上述的第三种方法。
在 v4 之前的版本中,一般是利用 require.ensure()
来实现代码分割的,而在 v4 中又是如何处理的呢?
在 React Router v4 官方给出的bundle-loader
的工具来实现这一功能。
其主要实现思路为创建一个名为 <Bundle>
的组件,当应用匹配到了对应的路径时,该组件会动态地引入所需模块并将自身渲染出来。
示例代码如下:
1 | import loadSomething from 'bundle-loader?lazy!./Something' |
更多关于 <Bundle>
组件的实现可参见上面给出的文档地址。
这里提到的两个缺点我们在实际开发工作中遇到的,与我们的项目特定结构相关,所以你可能并不会遇上。
一、 代码丑陋
由于我们的项目是从 React Router v2, v3升级过来的,在之前的版本中对于异步加载的实现采用了集中配置的方案,即项目中存在一个Routes.js
文件,整个项目的路径设置都放在了该文件中,这样方便集中管理。
但是在 React Router v4 版本中,由于使用了 bundle-loader
来实现代码分割,必须使用以下写法来引入组件:
1 | import loadSomething from 'bundle-loader?lazy!./Something' |
而我们的 reducer
和 saga
文件也需要使用此种方法引入,导致 Routes.js
文件顶端将会出现一长串及其冗长的组件引入代码,不易维护。如下所示:
当用这种方法引入的模块数量过多时,文件将会不忍直视。
二、 存在莫名的组件生命周期Bug
在使用了这种方案后,在某些页面中会出现这样的一个Bug:应用中进行页面跳转时,上一个页面的组件会在unmount
之后重新创建一次。表现为已经到了下一页面,但是会调用存在于跳转前页面中的组件的componentDidMount
方法。
当然,这个Bug只与我自己的特定项目有关,错误原因可能与bundle-loader
并无太大关联。不过因为一直无法解决这一问题,所以决定换一个方案来代替bundle-loader
。
Dan Abramov 在这个 create-react-app 的 issue 中给出了bundle-loader
的替代方案的链接:
一个常规的 React Router 项目结构如下:
1 | // 代码出处: |
首先根据我们的 route 引入相应的组件,然后将其用于定义相应的<Route>
。
但是,不管匹配到了哪一个route,我们这里都一次性地引入所有的组件。而我们想要的效果是当匹配了一个route,则只引入与其对应的组件,这就需要实现代码分割了。
异步组件,即只有在需要的时候才会引入。
1 | import React, { Component } from 'react'; |
asyncComponent
接收一个 importComponent
函数作为参数,importComponent()
在被调用时会动态引入给定的组件。
在 componentDidMount()
中,调用传入的importComponent()
,并将动态引入的组件保存在 state 中。
不再使用如下静态引入组件的方法:
1 | import Home from './containers/Home'; |
而是使用 asyncComponent
方法来动态引入组件:
1 | const AsyncHome = asyncComponent(() => import('./containers/Home')); |
此处的 import()
来自于新的 ES 提案,其结果是一个Promise,这是一种动态引入模块的方法,即上文 webpack文档中提到的第三种方法。更多关于 import()
的信息可以查看这篇文章:
注意这里并没有进行组件的引入,而是传给了 asyncComponent
一个函数,它将在 AsyncHome
组件被创建时进行动态引入。同时,这种传入一个函数作为参数,而非直接传入一个字符串的写法能够让webpack 意识到此处需要进行代码分割。
最后如下使用这个 AsyncHome
组件:
1 | <Route path="/" exact component={AsyncHome} /> |
在上面的这篇文章中,只给出了对于组件的异步引入的解决方案,而在我们的项目中还存在将reducer
和 saga
文件异步引入的需求。
1 | processReducer(reducer) { |
将需要异步引入的 reducer 作为参数传入,利用 Promise来对其进行异步处理。在 componentDidMount
方法中等待 reducer处理完毕后在将组件保存在 state 中,对于 saga 文件同理。
1 | // componentDidMount 中做如下修改 |
在上面对 reducer
文件进行处理时,使用了这样的一行代码:
1 | injectAsyncReducer(key, x.default); |
其作用是利用 Redux 中的 replaceReducer()
方法来修改reducer,具体代码见下。
1 | // reducerList 是你当前的 reducer 列表 |
完整的 asyncComponent 代码可见以下gist,注意一点,为了能够灵活地使用不同的injectAsyncReducer
, injectAsyncSaga
函数,代码中使用了高阶组件的写法,你可以直接使用内层的asyncComponent
函数。
考虑到代码可读性,可在你的 route
目录下新建一个asyncImport.js
文件,将需要异步引入的模块写在该文件中:
1 | // 引入前面所写的异步加载函数 |
然后在项目的 Router
组件中引用:
1 | // route/index.jsx |
根据 React Router v4 的哲学,React Router中的一切皆为组件,所以不必在一个单独的文件中统一配置所有的路由信息。建议在你最外层的容器组件,比如我的route/index.jsx
文件中只写入对应一个单独页面的容器组件,而页面中的子组件在该容器组件中异步引入并使用。
]]>
CodeSplitting in Create React App ES proposal:import() – dynamically importing ES modules
最近在写的项目中存在着社交模块,需要实现这样的一个功能:当发生了用户被点赞、评论、关注等操作时,需要由服务器向用户实时地推送一条消息。最终完成的项目地址为:
项目的流程中存在着这样的几个对象:
事件处理的流程如下:
上面的流程中,Java后端服务器是如何实现的不在此篇文章的讨论范围内,本文将主要介绍如何使用Node.js 来实现这个消息推送服务器。
考虑消息推送服务器上必须记录下当前在线用户的信息,这样才能向特定的用户推送消息。所以当用户登录时,必须将自身的用户信息发到Node.js 服务器上。为了达到这种双向的实时消息传递,很明显地考虑用WebSocket 来实现。既然我们在消息推送服务器上使用了Node.js,我们就有了一个很方便的选项:socket.io。
Socket.io是一个用 JavaScript实现的实时双向通信的库,利用它来实现我们的功能会很简单。
socket.io
包含两个部分:
可以看看如下的 socket.io
的示例代码,它给出了socket.io
发出及监听事件的基本用法:
1 | io.on('connection', function(socket){ |
关于 Socket.io 还有一点需要注意:Socke.io 并不完全是 WebSocket的实现。
Note: Socket.IO is not a WebSocket implementation. Although Socket.IOindeed uses WebSocket as a transport when possible, it adds somemetadata to each packet: the packet type, the namespace and the ack idwhen a message acknowledgement is needed.
接下来我们需要用 Express.js 来建立一个服务器端程序,并在其中引入Socket.io。
我们使用了 Express.js 来搭建 Node.js消息推送服务器,先利用一个简要的例子来浏览其功能:
1 | // server.js |
将上面的代码保存为 server.js
,新建一个public
文件夹,在其中放入 index.html
文件。运行以下命令:
node server.js
现在即可在 localhost:4001
查看效果了。
现在已经有了一个基础的 Express 服务器,接下来需要将 Socket.io加入其中。
1 | const io = require('socket.io')(http); |
这里的 io
监听 connection
事件,当client
与 server
建立了连接之后,这里的回调函数会被调用(client
中的代码将在下一节介绍)。
函数的参数 socket
代表的是当前的 client
和server
间建立的这个连接。可在 client
程序中将这个建立的 socket 连接打印出来,如下图所示:
其中的 id
属性可以用于标识出这一连接,从而server
可以向特定的用户发送消息。
1 | socket.broadcast.emit('new_user', {}); |
这一行代码表示 socket
将向当前所有与 server
建立了连接的 client
(不包括自己) 广播一条名为new_user
的消息。
在 Node 服务器建立一个用户信息和 socket id的映射表,因为同一用户可能打开了多个页面,所以他的 socket id可能存在多个值。当用户建立连接时,往其中添加值;用户断开连接后,删除相应值。
当 Java 后台存在需要推送的消息时,会向 Node 服务器的/api
路径 post 一条消息,其中包括用于标识用户的 tokenId和其它数据。
Node 服务器接收到 post 请求后,对请求内容进行处理。根据 tokenId找出与该用户对应的 socket id,socket.io 会根据 id来向用户推送消息。
方便起见,这里只用一个数组保存用户信息,实际工作中可以根据需要放入数据库中保存。
1 | global.users = []; // 记录下登录用户的tokenId, socketId |
当用户登录时,client
会向 server
发送user_login
事件,服务器接收到后会做如下操作:
1 | socket.on('user_login', function(info) { |
addSocketId()
会向 users
数组中添加用户信息,不同用户通过 tokenId 进行区分,每个用户有一个socketIds
数组,保存可能存在的多个socketId
。该函数的具体代码可见 src/utils.js
文件。
同理,还有一个 deleteSocketId()
函数用于删除用户信息,代码可见同一文件。
在获取了用户的 tokenId 之后,就需要找到对应的socketId,然后向特定用户推送消息。
1 | // 只向 id = socketId 的这一连接发送消息 |
服务器的思路大致如此,接下来介绍客户端中是如何进行相应的处理的。
首先在 html 文件中引入 Socket.io 的 client 端文件,例如通过 CDN引入:
1 | <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.3/socket.io.js"></script> |
其它的引入方式:
1 | <script src="/socket.io/socket.io.js"></script> |
1 | const io = require('socket.io-client'); |
引入 Socket.io 后就获得了 io
函数,通过它来与消息推送服务器建立连接。
1 | // 假设你将 Node 服务器部署后的地址为:https://www.example.com/ws |
如果监听本地:
1 | const msgSocket = io('http://localhost:4001'); |
这里如果写成 io('https://www.example.com/ws')
会出现错误,需要将 /ws
写入path中。
为了能在其它文件使用这一变量,可将 msgSocket
作为一个全局变量:
1 | window.msgSocket = msgSocket; |
1 | // 用户登录时,向服务器发送用户的信息。服务器会在收到信息后建立 socket 与用户的映射。 |
1 | // WebSocket 连接建立后,监听名为 receive_message 的事件 |
当 WebSocket 服务器向客户端推送了消息之后,客户端需要监听receive_message
事件,接收到的参数中有相应待处理的信息。由于使用了 Redux 进行数据的处理,所以这里 dispatch 了一个NEW_SOCKET_MSG
action,后续则是常规的 redux处理流程了。
GitHub 上的项目地址:
npm run dev
即可在 devlopment环境下进行测试,现在你就有了一个运行在4001端口的消息推送服务器了。
但是这里并没有后端的服务器来向我们发送消息,所以我们将利用 Postman来模拟发送消息。
为了展示程序的功能,在项目的 client 文件夹下放置了一个index.html
文件。注意这个文件并不能用在实际的项目中,只是用来显示消息推送的效果而已。
在开启了服务器之后,打开client/index.html
,根据提示随意输入一个 tokenId 即可。
现在利用 Postman 向 localhost:4001/api
post如下的一条信息:
{ // tokens 数组表示你想向哪个用户推送消息 "tokens": ["1", "2"], "data": "You shall not pass!!!"}
至此,如果一切顺利,你应该能够在 client的控制台中看到收到的消息了。
你可以打开多个 client 页面,输入不同的tokenId,然后检查消息是否发送给了正确的用户。
]]>https://github.com/socketio/socket.io/tree/master/examples/chathttps://socket.io/docs/
看了几个她在codepen上的作品,比如这个
不过有哪个动漫中的人物足够简单,能够用几个基本的几何图形就画出来呢?我想到了一个人,于是我决定画一个《一拳超人》中的卤蛋,不对,是秃头披风侠——琦玉老师。
结果展示:
See the Pen One punchman by wu kai (
从html文件中你可以看到这张图片实际上全部是由div
元素组合而成的,一共用到了15个div。在给一个div元素加上适当的css样式后,就形成了脸上的一个部位。
在绘制琦玉的头像时,最重要的一个css属性就是border-radius
,我们用它可以画出圆形、椭圆及各种变体。图中的脸部轮廓、眼睛、耳朵的形状都是由border-radius
来实现的,稍后将介绍其使用方法。
另一个需要说明的css属性是transform
,可以实现平移和旋转。
之前我对border-radius
的认识只局限于可以给元素加上圆角,以及将其值设为50%可以让矩形显示为圆形。查了些资料后,才发现可以用它画出许多图形。
border-radius
是以下四个属性的简写,每一个属性用于设置一个角的形状:
border-top-left-radiusborder-top-right-radiusborder-bottom-right-radiusborder-bottom-left-radius
图片来自
从上图可以看出当只设置一个值或设置两个相同的值时,显示为圆;设置两个不同的值时,显示为椭圆。以border-top-left-radius
为例:
1 | /* the corner is a circle */ |
若是简写形式,则写成如下格式:
1 | border-radius: 48% 48% 50% 50% / 42% 42% 54% 54%; |
'/'之前的四个值表示水平轴的长度,'/'之后的四个值表示垂直轴的长度,当水平轴的长度和垂直轴的长度相等时,可以省略'/'及之后的这一组值。
对于同一组的四个数值,也有简写方式。方法与 padding 和 margin的简写类似,第一个值与第三个值相同或第二个值与第四个值相同时,可以只写一遍。
在了解了border-radius
的用法后,通过给div
元素合适的宽高比,在调整四个圆角的半径,就能够获得你想要的形状了。
1 | // 以下的样式能够画出琦玉的脸部形状 |
1 | /* 可以用 translate 来实现平移操作 */ |
在你已经将琦玉头像进行拆解,把各个部分都用一个div
来表示并加上了合适的样式后,就能将它们组合起来了。你可以用transform
来调整它们的距离,或者直接用absoulte
定位。
最后就得到了琦玉的头像:
额,秃子,你谁啊!?
看来不是所有的光头都叫琦玉,还需要对细节进行一点调整:
OK,这样就有点像琦玉老师了。最后,如果你愿意的话,还可以用transition
属性来稍微加上点动画效果。
再放一遍代码地址:
]]>
Codepen代码及展示
这门课程系统地介绍了前端开发中的许多工具和术语,从编辑器的选择到项目的部署都进行了介绍,我在学习了这个课程后感觉将之前零散学习的内容串了起来。
顺便一说,Pluralsight网站上面有许多不错的课程,不过价格太贵,需每月付29美元😥。但是,你可以用微软账号加入
编程的时候要做许多选择
这门课程介绍了如何在这些选项中做出选择
原因:
Atwood's Law
JavaScript is Everywhere
Rhythm 1. Options 2. Recommendation 3. Implement
可选编辑器: - Atom - WebStorm - Brackets - VSCode
EditorConfig 保证大家的编辑器设置一样。
增加 .editorconfig 文件
何时 Run Security Check ?
npm start
全局安装 nsp
nsp install -g nspλ nsp check(+) No known vulnerabilities found
步骤:
1. npm install localtunnel -g2. start app3. lt --port 3000
快速将静态文件部署到公开URL上去。
1 | scripts: { |
当运行 npm start
时,prestart
命令中的内容会自动在其之前运行。
在 scripts 项中加入:
"security-check": "nsp check"
假设在运行server的同时,进行security-check,对start命令进行如下的修改:
"start": "npm-run-all --parallel security-check open:src","open:src": "node buildScripts/srcServer.js",
其中用 npm-run-all 来代替。
可以用 npm start -s 减少输出中的噪声。
Popular transpilers:
Babel可以将最新版本的JavaScript代码向下转换成ES5,从而可以使用所有的新特性。也是最流行的。
TypeScript是JavaScript的超集。
Elm不是JavaScript,可以编译成JS。
Babel配置风格:
使用.babelrc文件:不止能用在npm中,独立文件易于阅读
package.json:项目中少了一个文件
在文件中加入babel
项即可
1 | { |
加入 .babelrc文件
1 | { |
在preset命令中用babel-node来代替node,来使用es6语法。这样可以使用import,const 等关键字。
1 | "prestart": "babel-node buildScripts/startMessage.js", |
CommonJS不能在web浏览器中工作,需要将npmpackage打包成浏览器能够使用的形式。
5中模块形式: - IIFE - AMD - CommonJS - UMD - ES6 Modules
现在应继续使用的两种模块:
CommonJS: node中使用
var jquery = require('jquery')
ES6 Module
原因:
Why webpack? - Much more than just js - css - images - fonts - html -Bundle splitting - Hot module reloading - Webpack2 offers treeshaking
在根目录下新建webpack.config.dev.js
文件,内容如下:
1 | import path from 'path'; |
Note: Webpack won't actually generate any physical files for ourdevelopment build. It will serve our build from memory.
在 buildScripts/srcServer.js
中添加下列内容
1 | import webpack from 'webpack'; |
添加index.js
文件
1 | // src/index.js |
1 | <script src="bundle.js"></script> |
在我们的webpack配置文件中已经加入了css的loader的情况下,只需要在index.js引入css文件即可。
1 | import './index.css'; |
Why: Debug transpiled and bundled code.
Maps code back to original source
Linter选择:
创建ESLint配置文件,或在package.json
文件中加入eslintConfig
项。
React开发推荐:eslint-plugin-react
Issue: ESLint doesn't watch files
解决方案:
We'll use plain ESLint and run it via eslint-watch.
ESLint设置:
添加.eslintrc.json
文件,内容如下:
创建调用eslint-watch
的 npm script:
"lint": "esw webpack.config.* src buildScripts --color",
可以使用注释来排除某一个eslint的规则:
/* eslint-disable no-console */// eslint-disable-line no-console
在 lint script 下加一句:
"lint:watch": "npm run lint -- --watch",
加入 start script 中:
"start": "npm-run-all --parallel security-check open:src lint:watch",
Style | Focus |
---|---|
Unit | Single function or module |
Integration | Interations between modules |
UI | Automate interaations with UI |
以上任一皆可。
Jasmine and Jest have assertion libries built in.
Assertion: Declare what you expect
eg. expect(2 + 2).to.equal(4)
JSOM: Simulate the browser's DOM
Cheerio: jQuery for the server
1 | /** buildScripts/testSetup.js */ |
1 | // add "test" scripts in package.json |
1 | /* src/index.test.js */ |
1 | import jsdom from 'jsdom'; |
Add this script to package.json
:
"test:watch": "npm run test -- --watch"
Then add test:watch
to the end of start
script:
"start": "npm-run-all --parallel security-check open:src lint:watch test:watch"
When team commits code, confirm immediately that the commit works asexpected when on another machine.
使用GitHub账号登录TravisCI,即可选择你在GitHub上的项目进行持续集成。
需要在项目中加入travis的配置文件.travis.yml
:
language: node_jsnode_js: - "6"
Why?
修改 srcServer.js
文件
1 | app.get('/users', function(req, res) { |
加入 userApi.js 文件
1 | import 'whatwg-fetch'; |
此时打开localhost:3000/users
,能查看API结果。
Polyfill.io: Only send polyfill to those who need it
JSON Schema Faker
JSON Server
创建文件:buildScripts/mockDataSchema.js
文件,内容如下:bit.ly/ps-mock-data-schema
在buildScripts
中新建文件generateMockData.js
:
1 | // buildScripts/generateMockData.js |
在package.json
中加入generate-mock-data
script:
"generate-mock-data": "babel-node buildScripts/generateMockData"
运行npm run generate-mock-data
,会生成一个db.json
文件。
Add script:
"start-mockapi": "json-server --watch src/api/db.json --port 3001"
json-server会将db.json
中的内容生成一个API,如下为运行后输出内容。
Loading src/api/db.jsonDoneResourceshttp://localhost:3001/usersHomehttp://localhost:3001
在开发环境中需要将API换成 mock api。
1 | // src/api |
1 | function get(url) { |
1 | // src/api |
Examples of: - Directory structure and file naming - Framework usage- Testing - Mock API - Automated deployment
1 | <script> |
POJOs: Plain Old JavaScript Objects, Pure logic, Noframework-specific code
新建webpack.config.prod.js
,相比webpack.config.dev.js
加入plugin:
1 | plugins: [ |
新建buildScripts/build.js
文件。
新建buildScripts/distServer.js
文件,用于 staticfiles。