跳到主要内容

3 篇博文 含有标签「Web」

查看所有标签

「知乎」为什么 CSS 这么难学?

· 阅读需 4 分钟
Hux

这篇文章转载自我在知乎上的回答

对我来说,CSS 难学以及烦人是因为它**「出乎我意料之外的复杂」且让我觉得「定位矛盾」**。

@方应杭 老师的答案我赞了:CSS 的属性互不正交,大量的依赖与耦合难以记忆。

@顾轶灵 @王成 说得也没错:CSS 的很多规则是贯彻整个体系的,而且都记在规范里了,是有规律的,你应该好好读文档而不是去瞎试。

CSS是一门正儿八经的编程语言,请拿出你学C++或者Java的态度对待它

但是问题就在这了,无论从我刚学习前端还是到现在,我都没有把 CSS 作为一门正儿八经的编程语言(而且显然图灵不完全的它也不是),CSS 在我眼里一直就是一个布局、定义视觉样式用的 DSL,与 HTML 一样就是一个标记语言。

写 CSS 很有趣,CSS 中像继承、类、伪类这样的设计确实非常迎合程序员的思路,各种排列组合带来了很多表达上的灵活性。但如果可以选择,在生产环境里我更愿意像 iOS/Android/Windows 开发那样,把这门 DSL 作为 IDE WYSIWYG 编辑器的编译目标就可以了,当然你可以直接编辑生成的代码,但我希望「对于同一种效果,有比较确定的 CSS 表达方式」

因为我并不在 CSS 里处理数据结构,写算法、业务逻辑啊,我就是希望我能很精确得表达我想要的视觉效果就可以了。如果我需要更复杂的灵活性和控制,你可以用真正的编程语言来给我暴露 API,而不是在 CSS 里给我更多的「表达能力」

CSS 语言本身的表达能力对于布局 DSL 来说是过剩的,所以你仅仅用 CSS 的一个很小的子集就可以在 React Native 里搞定 iOS/Android 的布局了。你会发现各个社区(典型如 React)、团队都要花很多时间去找自己项目适合的那个 CSS 子集(so called 最佳实践)。而且 CSS 的这种复杂度其实还挺严重得影响了浏览器的渲染性能,很多优化变得很难做。

而 CSS 的表达能力对于编程语言来说又严重不够,一是语言特性不够,所以社区才会青睐 Less、Sass 这些编译到 CSS 的语言,然后 CSS 自己也在加不痛不痒的 Variable。二是 API 不够,就算你把规范读了,你会发现底层 CSSOM 的 Layout、Rendering 的东西你都只能强行用声明式的方式去 hack(比如用 transform 开新的 composition layer)而没有真正的 API 可以用,所以 W3C 才会去搞 Houdini 出来。

这种不上不下的感觉就让我觉得很「矛盾」,你既没法把 CSS 当一个很简单的布局标记语言去使用,又没办法把它作为一个像样的编程语言去学习和使用。

在写 CSS 和 debug CSS 的时候我经常处在一种「MD 就这样吧反正下次还要改」和「MD 这里凭什么是这样的我要研究下」的精分状态,可是明明我写 CSS 最有成就感的时候是看到漂亮的 UI 啊。

以上。

How does SW-Precache works?

· 阅读需 10 分钟
Hux

SW-Precache is a great Service Worker tool from Google. It is a node module designed to be integrated into your build process and to generate a service worker for you. Though you can use sw-precache out of the box, you might still wonder what happens under the hood. There you go, this article is written for you!

This post was first published at Medium

Overview

The core files involving in sw-precache are mainly three:

service-worker.tmpl  
lib/
├ sw-precache.js
└ functions.js

sw-precache.js is the main entry of the module. It reads the configuration, processes parameters, populates the service-worker.tmpl template and writes the result into specified file. Andfunctions.js is just a module containing bunch of external functions which would be all injected into the generated service worker file as helpers.

Since the end effect of sw-precache is performed by the generated service worker file in the runtime, a easy way to get an idea of what happens is by checking out source code inside service-worker.tmpl . It’s not hard to understand the essentials and I will help you.

Initialization

The generated service worker file (let’s call it sw.js for instance) get configuration by text interpolation when sw-precache.js populating service-worker.tmpl .

// service-worker.tmpl  
var precacheConfig = <%= precacheConfig %>;

// sw.js
var precacheConfig = [
["js/a.js", "3cb4f0"],
["css/b.css", "c5a951"]
]

It’s not difficult to see that it’s a list of relative urls and MD5 hashes. In fact, one thing that sw-precache.js do in the build time is to calculate hash of each file that it asked to “precache” from staticFileGlobs parameter.

In sw.js, precacheConfig would be transformed into a ES6 Map with structure Map {absoluteUrl => cacheKey} as below. Noticed that I omit the origin part (e.g. http://localhost) for short.

> urlToCacheKeys  
< Map(2) {
"http.../js/a.js" => "http.../js/a.js?_sw-precache=3cb4f0",
"http.../css/b.js" => "http.../css/b.css?_sw-precache=c5a951"
}

Instead of using raw URL as the cache key, sw-precache append a _sw-precache=[hash] to the end of each URL when populating, updating its cache and even fetching these subresouces. Those _sw-precache=[hash] are what we called cache-busting parameter*. It can prevent service worker from responding and caching out-of-date responses found in browsers’ HTTP cache indefinitely.

Because each build would re-calculate hashes and re-generate a new sw.js with new precacheConfig containing those new hashes, sw.js can now determine the version of each subresources thus decide what part of its cache needs a update. This is pretty similar with what we commonly do when realizing long-term caching with webpack or gulp-rev, to do a byte-diff ahead of runtime.

*: Developer can opt out this behaviour with dontCacheBustUrlsMatching option if they set HTTP caching headers right. More details on Jake’s Post.

On Install

ServiceWorker gives you an install event. You can use this to get stuff ready, stuff that must be ready before you handle other events.

During the install lifecycle, sw.js open the cache and get started to populate its cache. One cool thing that it does for you is its incremental update mechanism.

Sw-precache would search each cache key (the values of urlsToCacheKeys) in the cachedUrls, a ES6 Set containing URLs of all requests indexed from current version of cache, and only fetch and cache.put resources couldn’t be found in cache, i.e, never be cached before, thus reuse cached resources as much as possible.

If you can not fully understand it, don’t worry. We will recap it later, now let’s move on.

On Activate

Once a new ServiceWorker has installed & a previous version isn’t being used, the new one activates, and you get an activate event. Because the old version is out of the way, it's a good time to handle schema migrations in IndexedDB and also delete unused caches.

During activation phase, sw.js would compare all existing requests in the cache, named existingRequests (noticed that it now contains resources just cached on installation phase) with setOfExpectedUrls, a ES6 Set from the values of urlsToCacheKeys. And delete any requests not matching from cache.

// sw.js
existingRequests.map(function(existingRequest) {
if (!setOfExpectedUrls.has(existingRequest.url)) {
return cache.delete(existingRequest);
}
})

On Fetch

Although the comments in source code have elaborated everything well, I wanna highlight some points during the request intercepting duration.

Should Respond?

Firstly, we need to determine whether this request was included in our “pre-caching list”. If it was, this request should have been pre-fetched and pre-cached thus we can respond it directly from cache.

// sw.js*  
var url = event.request.url
shouldRespond = urlsToCacheKeys.has(url);

Noticed that we are matching raw URLs (e.g. http://localhost/js/a.js) instead of the hashed ones. It prevent us from calculating hashes at runtime, which would have a significant cost. And since we have kept the relationship in urlToCacheKeys it’s easy to index the hashed one out.

* In real cases, sw-precache would take ignoreUrlParametersMatching and directoryIndex options into consideration.

One interesting feature that sw-precache provided is navigationFallback(previously defaultRoute), which detect navigation request and respond a preset fallback HTML document when the URL of navigation request did not exist in urlsToCacheKeys.

It is presented for SPA using History API based routing, allowing responding arbitrary URLs with one single HTML entry defined in navigationFallback, kinda reimplementing a Nginx rewrite in service worker*. Do noticed that service worker only intercept document (navigation request) inside its scope (and any resources referenced in those documents of course). So navigation towards outside scope would not be effected.

* navigateFallbackWhitelist can be provided to limit the “rewrite” scope.

Respond from Cache

Finally, we get the appropriate cache key (the hashed URL) by raw URL with urlsToCacheKeys and invoke event.respondWith() to respond requests from cache directly. Done!

// sw.js*
event.respondWith(
caches.open(cacheName).then(cache => {
return cache.match(urlsToCacheKeys.get(url))
.then(response => {
if (response) return response;
});
})
);

* The code was “ES6-fied” with error handling part removed.

Cache Management Recap

That’s recap the cache management part with a full lifecycle simulation.

The first build

Supposed we are in the very first load, the cachedUrls would be a empty set thus all subresources listed to be pre-cached would be fetched and put into cache on SW install time.

// cachedUrls  
Set(0) {}

// urlToCacheKeys
Map(2) {
"http.../js/a.js" => "http.../js/a.js?_sw-precache=3cb4f0",
"http.../css/b.js" => "http.../css/b.css?_sw-precache=c5a951"
}

// SW Network Logs
[sw] GET a.js?_sw-precache=3cb4f0
[sw] GET b.css?_sw-precache=c5a951

After that, it will start to control the page immediately because the sw.js would call clients.claim() by default. It means the sw.js will start to intercept and try to serve future fetches from caches, so it’s good for performance.

In the second load, all subresouces have been cached and will be served directly from cache. So none requests are sent from sw.js.

// cachedUrls  
Set(2) {
"http.../js/a.js? _sw-precache=3cb4f0",
"http.../css/b.css? _sw-precache=c5a951"
}

// urlToCacheKeys
Map(2) {
"http.../js/a.js" => "http.../js/a.js? _sw-precache=3cb4f0",
"http.../css/b.js" => "http.../css/b.css? _sw-precache=c5a951"
}

// SW Network Logs
// Empty

The second build

Once we create a byte-diff of our subresouces (e.g., we modify a.js to a new version with hash value d6420f) and re-run the build process, a new version of sw.js would be also generated.

The new sw.js would run alongside with the existing one, and start its own installation phase.

// cachedUrls  
Set(2) {
"http.../js/a.js? _sw-precache=3cb4f0",
"http.../css/b.css? _sw-precache=c5a951"
}

// urlToCacheKeys
Map(2) {
"http.../js/a.js" => "http.../js/a.js? _sw-precache=d6420f",
"http.../css/b.js" => "http.../css/b.css? _sw-precache=c5a951"
}

// SW Network Logs
[sw] GET a.js?_sw-precache=d6420f

This time, sw.js see that there is a new version of a.js requested, so it fetch /js/a.js?_sw-precache=d6420f and put the response into cache. In fact, we have two versions of a.js in cache at the same time in this moment.

// what's in cache?
http.../js/a.js?_sw-precache=3cb4f0
http.../js/a.js?_sw-precache=d6420f
http.../css/b.css?_sw-precache=c5a951

By default, sw.js generated by sw-precache would call self.skipWaiting so it would take over the page and move onto activating phase immediately.

// existingRequests
http.../js/a.js?_sw-precache=3cb4f0
http.../js/a.js?_sw-precache=d6420f
http.../css/b.css?_sw-precache=c5a951

// setOfExpectedUrls
Set(2) {
"http.../js/a.js?_sw-precache=d6420f",
"http.../css/b.css?_sw-precache=c5a951"
}

// the one deleted
http.../js/a.js?_sw-precache=3cb4f0

By comparing existing requests in the cache with set of expected ones, the old version of a.js would be deleted from cache. This ensure there is only one version of our site’s resources each time.

That’s it! We finish the simulation successfully.

Conclusions

As its name implied, sw-precache is designed specifically for the needs of precaching some critical static resources. It only does one thing but does it well. I’d love to give you some opinionated suggestions but you decide whether your requirements suit it or not.

Precaching is NOT free

So don’t precached everything. Sw-precache use a “On Install — as a dependency” strategy for your precache configs. A huge list of requests would delay the time service worker finishing installing and, in addition, wastes users’ bandwidth and disk space.

For instance, if you wanna build a offline-capable blogs. You had better not include things like 'posts/*.html in staticFileGlobs. It would be a huge disaster to data-sensitive people if you have hundreds of posts. Use a Runtime Caching instead.

“App Shell”

A helpful analogy is to think of your App Shell as the code and resources that would be published to an app store for a native iOS or Android application.

Though I always consider that the term “App Shell” is too narrow to cover its actual usages now, It is widely used and commonly known. I personally prefer calling them “Web Installation Package” straightforward because they can be truly installed into users’ disks and our web app can boot up directly from them in any network environments. The only difference between “Web Installation Package” and iOS/Android App is that we need strive to limit it within a reasonable size.

Precaching is perfect for this kinda resources such as entry html, visual placeholders, offline pages etc., because they can be static in one version, small-sized, and most importantly, part of critical rendering path. We wanna put first meaningful paint ASAP to our user thus we precache them to eliminate HTTP roundtrip time.

BTW, if you are using HTML5 Application Cache before, sw-precache is really a perfect replacement because it can cover nearly all use cases the App Cache provide.

This is not the end

Sw-precache is just one of awesome tools that can help you build service worker. If you are planing to add some service worker power into your website, Don’t hesitate to checkout sw-toolbox, sw-helper (a new tool Google is working on) and many more from communities.

That’s all. Wish you enjoy!

「知乎」如何理解 <code>document</code> 对象是 <code>HTMLDocument</code> 的实例?

· 阅读需 2 分钟
Hux

这篇文章转载自我在知乎上的回答

谢邀。

首先要理解的是 DOM 是 API,是一组无关编程语言的接口(Interfaces)而非实现(Implementation)。前端平时常说的 DOM 其实只是浏览器通过 ECMAScript(JavaScript)对 DOM 接口的一种实现。

其次要知道的是,DOM 既是为 HTML 制定的,也是为 XML 制定的。而两者各有一些特异的部分,所以作为 DOM 标准基石的 DOM Level 1 其实分为 Core 与 HTML 两个部分。Core 定义了 fundamental interfaces 与 extended interfaces,分别是共用的基础接口与 「XML 拓展包」,而 HTML 部分则全都是「HTML 拓展包」。题主所问到的 Document 接口被定义在 Core 的 fundamental interfaces 中,而 HTMLDocument 接口则定义在 HTML 部分中,且「接口继承」于 Document。

这种继承关系当然是可以在 JavaScript 的 DOM 实现中体现出来的:

// document 是 HTMLDocument 的实例
document instanceof HTMLDocument // true

// document 的 [[prototype]] 指向 HTMLDocument 的原型
document.__proto__ === HTMLDocument.prototype // true

// HTMLDocument 伪类继承于 Document
HTMLDocument.prototype instanceof Document // true
HTMLDocument.prototype.__proto__ === Document.prototype // true

至于 Document 与 HTMLDocument 这两个构造函数,跟 Array、Object 一样都是 built-in 的:

> Document
< function Document() { [native code] }
> HTMLDocument
< function HTMLDocument() { [native code] }

虽然是 native code,但一个有意思的现象是,这两个构造函数之间也是存在原型链的:

// HTMLDocument 的 [[prototype]] 是指向 Document 的
HTMLDocument.__proto__ == Document

// 同理
Document.__proto__ == Node
Node.__proto__ == EventTarget

其作用是实现对静态成员的继承。( ES6 Class 的行为与此完全一致,但这个行为在更早之前就是这样了。)

好了扯远了,总结一下,在 JavaScript 的 DOM 实现中

  • document 是 HTMLDocument 的实例
  • HTMLDocument 继承于 Document

留一个课后作业,有兴趣的话可以看看 Document.prototype 与 HTMLDocument.prototype 里分别都有什么?在不同浏览器里都试试。

以上。