Repository: lavas-project/pwa-book Branch: master Commit: 529311ee4da8 Files: 536 Total size: 11.1 MB Directory structure: gitextract_nvjmu4af/ ├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── SUMMARY.md ├── appendix01/ │ ├── 1-lighthouse.md │ ├── 2-lighthouse-score-guide.md │ └── 3-lighthouse-case.md ├── appendix01.md ├── book.json ├── chapter01/ │ ├── 1-how-was-pwa-born.md │ ├── 2-what-is-pwa.md │ ├── 3-what-are-key-techs.md │ ├── 4-how-is-pwa-going.md │ ├── 5-the-future-of-pwa.md │ └── 6-your-first-pwa.md ├── chapter01.md ├── chapter02/ │ ├── 1-what-is-good-ux.md │ ├── 2-design-and-tech.md │ ├── 3-app-shell.md │ ├── 4-app-skeleton.md │ └── 5-responsive-design.md ├── chapter02.md ├── chapter03/ │ ├── 1-promise.md │ ├── 2-async-function.md │ ├── 3-fetch-api.md │ ├── 4-cache-api.md │ └── 5-indexeddb.md ├── chapter03.md ├── chapter04/ │ ├── 1-service-worker-introduction.md │ ├── 2-service-worker-register.md │ ├── 3-service-worker-dive.md │ ├── 4-service-worker-debug.md │ └── code/ │ ├── applicationCacheDemo/ │ │ ├── index.html │ │ └── manifest.appcache │ ├── serviceWorkerCacheDemo/ │ │ ├── data.json │ │ ├── index.css │ │ ├── index.html │ │ ├── index.js │ │ └── sw.js │ ├── serviceWorkerDemo/ │ │ ├── index.html │ │ └── sw.js │ ├── serviceWorkerIndexeddbDemo/ │ │ ├── fruits.json │ │ ├── index.html │ │ └── sw.js │ ├── serviceWorkerLifecycleDemo/ │ │ ├── index.html │ │ └── sw.js │ ├── serviceWorkerScopeDemo/ │ │ ├── a/ │ │ │ └── b/ │ │ │ └── sw.js │ │ └── index.html │ ├── serviceWorkerScopeDemo1/ │ │ ├── a/ │ │ │ ├── a-sw.js │ │ │ └── index.html │ │ ├── b/ │ │ │ └── index.html │ │ └── root-sw.js │ └── serviceWorkerUnregisterDemo/ │ ├── index.html │ └── sw.js ├── chapter04.md ├── chapter05/ │ ├── 1-fetch-event-management.md │ ├── 2-local-storage-management.md │ ├── 3-respond-strategy.md │ ├── 4-precache.md │ └── 5-workbox.md ├── chapter05.md ├── chapter06/ │ ├── 1-manifest-json.md │ ├── 2-credentials-api.md │ ├── 3-notification-api.md │ ├── 4-web-push-api.md │ └── 5-payment-request-api.md ├── chapter06.md ├── chapter07/ │ ├── 1-https.md │ ├── 2-CSP.md │ ├── 3-policy.md │ └── 4-vulnerability.md ├── chapter07.md ├── chapter08/ │ ├── 1-loading-performance.md │ └── 2-rendering-performance.md ├── chapter08.md ├── chapter09/ │ ├── 1-search-engine-index.md │ ├── 2-pwa-and-amp-and-mip.md │ ├── 3-whole-site-amp-and-mip.md │ └── 4-preload-pwa.md ├── chapter09.md ├── ci.yml ├── docs/ │ ├── LICENSE │ ├── appendix01/ │ │ ├── 1-lighthouse.html │ │ ├── 2-lighthouse-score-guide.html │ │ └── 3-lighthouse-case.html │ ├── appendix01.html │ ├── chapter01/ │ │ ├── 1-how-was-pwa-born.html │ │ ├── 2-what-is-pwa.html │ │ ├── 3-what-are-key-techs.html │ │ ├── 4-how-is-pwa-going.html │ │ ├── 5-the-future-of-pwa.html │ │ └── 6-your-first-pwa.html │ ├── chapter01.html │ ├── chapter02/ │ │ ├── 1-what-is-good-ux.html │ │ ├── 2-design-and-tech.html │ │ ├── 3-app-shell.html │ │ ├── 4-app-skeleton.html │ │ └── 5-responsive-design.html │ ├── chapter02.html │ ├── chapter03/ │ │ ├── 1-promise.html │ │ ├── 2-async-function.html │ │ ├── 3-fetch-api.html │ │ ├── 4-cache-api.html │ │ └── 5-indexeddb.html │ ├── chapter03.html │ ├── chapter04/ │ │ ├── 1-service-worker-introduction.html │ │ ├── 2-service-worker-register.html │ │ ├── 3-service-worker-dive.html │ │ ├── 4-service-worker-debug.html │ │ └── code/ │ │ ├── applicationCacheDemo/ │ │ │ ├── index.html │ │ │ └── manifest.appcache │ │ ├── serviceWorkerCacheDemo/ │ │ │ ├── data.json │ │ │ ├── index.css │ │ │ ├── index.html │ │ │ ├── index.js │ │ │ └── sw.js │ │ ├── serviceWorkerDemo/ │ │ │ ├── index.html │ │ │ └── sw.js │ │ ├── serviceWorkerIndexeddbDemo/ │ │ │ ├── fruits.json │ │ │ ├── index.html │ │ │ └── sw.js │ │ ├── serviceWorkerLifecycleDemo/ │ │ │ ├── index.html │ │ │ └── sw.js │ │ ├── serviceWorkerScopeDemo/ │ │ │ ├── a/ │ │ │ │ └── b/ │ │ │ │ └── sw.js │ │ │ └── index.html │ │ ├── serviceWorkerScopeDemo1/ │ │ │ ├── a/ │ │ │ │ ├── a-sw.js │ │ │ │ └── index.html │ │ │ ├── b/ │ │ │ │ └── index.html │ │ │ └── root-sw.js │ │ └── serviceWorkerUnregisterDemo/ │ │ ├── index.html │ │ └── sw.js │ ├── chapter04.html │ ├── chapter05/ │ │ ├── 1-fetch-event-management.html │ │ ├── 2-local-storage-management.html │ │ ├── 3-respond-strategy.html │ │ ├── 4-precache.html │ │ └── 5-workbox.html │ ├── chapter05.html │ ├── chapter06/ │ │ ├── 1-manifest-json.html │ │ ├── 2-credentials-api.html │ │ ├── 3-notification-api.html │ │ ├── 4-web-push-api.html │ │ └── 5-payment-request-api.html │ ├── chapter06.html │ ├── chapter07/ │ │ ├── 1-https.html │ │ ├── 2-CSP.html │ │ ├── 3-policy.html │ │ └── 4-vulnerability.html │ ├── chapter07.html │ ├── chapter08/ │ │ ├── 1-loading-performance.html │ │ └── 2-rendering-performance.html │ ├── chapter08.html │ ├── chapter09/ │ │ ├── 1-search-engine-index.html │ │ ├── 2-pwa-and-amp-and-mip.html │ │ ├── 3-whole-site-amp-and-mip.html │ │ └── 4-preload-pwa.html │ ├── chapter09.html │ ├── ci.yml │ ├── gitbook/ │ │ ├── fonts/ │ │ │ └── fontawesome/ │ │ │ └── FontAwesome.otf │ │ ├── gitbook-plugin-3-ba/ │ │ │ └── plugin.js │ │ ├── gitbook-plugin-ace/ │ │ │ ├── ace/ │ │ │ │ ├── ace.js │ │ │ │ ├── ext-beautify.js │ │ │ │ ├── ext-chromevox.js │ │ │ │ ├── ext-elastic_tabstops_lite.js │ │ │ │ ├── ext-emmet.js │ │ │ │ ├── ext-error_marker.js │ │ │ │ ├── ext-keybinding_menu.js │ │ │ │ ├── ext-language_tools.js │ │ │ │ ├── ext-linking.js │ │ │ │ ├── ext-modelist.js │ │ │ │ ├── ext-old_ie.js │ │ │ │ ├── ext-searchbox.js │ │ │ │ ├── ext-settings_menu.js │ │ │ │ ├── ext-spellcheck.js │ │ │ │ ├── ext-split.js │ │ │ │ ├── ext-static_highlight.js │ │ │ │ ├── ext-statusbar.js │ │ │ │ ├── ext-textarea.js │ │ │ │ ├── ext-themelist.js │ │ │ │ ├── ext-whitespace.js │ │ │ │ ├── keybinding-emacs.js │ │ │ │ ├── keybinding-vim.js │ │ │ │ ├── mode-abap.js │ │ │ │ ├── mode-abc.js │ │ │ │ ├── mode-actionscript.js │ │ │ │ ├── mode-ada.js │ │ │ │ ├── mode-apache_conf.js │ │ │ │ ├── mode-applescript.js │ │ │ │ ├── mode-asciidoc.js │ │ │ │ ├── mode-assembly_x86.js │ │ │ │ ├── mode-autohotkey.js │ │ │ │ ├── mode-batchfile.js │ │ │ │ ├── mode-c9search.js │ │ │ │ ├── mode-c_cpp.js │ │ │ │ ├── mode-cirru.js │ │ │ │ ├── mode-clojure.js │ │ │ │ ├── mode-cobol.js │ │ │ │ ├── mode-coffee.js │ │ │ │ ├── mode-coldfusion.js │ │ │ │ ├── mode-csharp.js │ │ │ │ ├── mode-css.js │ │ │ │ ├── mode-curly.js │ │ │ │ ├── mode-d.js │ │ │ │ ├── mode-dart.js │ │ │ │ ├── mode-diff.js │ │ │ │ ├── mode-django.js │ │ │ │ ├── mode-dockerfile.js │ │ │ │ ├── mode-dot.js │ │ │ │ ├── mode-eiffel.js │ │ │ │ ├── mode-ejs.js │ │ │ │ ├── mode-elixir.js │ │ │ │ ├── mode-elm.js │ │ │ │ ├── mode-erlang.js │ │ │ │ ├── mode-forth.js │ │ │ │ ├── mode-ftl.js │ │ │ │ ├── mode-gcode.js │ │ │ │ ├── mode-gherkin.js │ │ │ │ ├── mode-gitignore.js │ │ │ │ ├── mode-glsl.js │ │ │ │ ├── mode-golang.js │ │ │ │ ├── mode-groovy.js │ │ │ │ ├── mode-haml.js │ │ │ │ ├── mode-handlebars.js │ │ │ │ ├── mode-haskell.js │ │ │ │ ├── mode-haxe.js │ │ │ │ ├── mode-html.js │ │ │ │ ├── mode-html_elixir.js │ │ │ │ ├── mode-html_ruby.js │ │ │ │ ├── mode-ini.js │ │ │ │ ├── mode-io.js │ │ │ │ ├── mode-jack.js │ │ │ │ ├── mode-jade.js │ │ │ │ ├── mode-java.js │ │ │ │ ├── mode-javascript.js │ │ │ │ ├── mode-json.js │ │ │ │ ├── mode-jsoniq.js │ │ │ │ ├── mode-jsp.js │ │ │ │ ├── mode-jsx.js │ │ │ │ ├── mode-julia.js │ │ │ │ ├── mode-latex.js │ │ │ │ ├── mode-lean.js │ │ │ │ ├── mode-less.js │ │ │ │ ├── mode-liquid.js │ │ │ │ ├── mode-lisp.js │ │ │ │ ├── mode-live_script.js │ │ │ │ ├── mode-livescript.js │ │ │ │ ├── mode-logiql.js │ │ │ │ ├── mode-lsl.js │ │ │ │ ├── mode-lua.js │ │ │ │ ├── mode-luapage.js │ │ │ │ ├── mode-lucene.js │ │ │ │ ├── mode-makefile.js │ │ │ │ ├── mode-markdown.js │ │ │ │ ├── mode-mask.js │ │ │ │ ├── mode-matlab.js │ │ │ │ ├── mode-maze.js │ │ │ │ ├── mode-mel.js │ │ │ │ ├── mode-mips_assembler.js │ │ │ │ ├── mode-mipsassembler.js │ │ │ │ ├── mode-mushcode.js │ │ │ │ ├── mode-mysql.js │ │ │ │ ├── mode-nix.js │ │ │ │ ├── mode-objectivec.js │ │ │ │ ├── mode-ocaml.js │ │ │ │ ├── mode-pascal.js │ │ │ │ ├── mode-perl.js │ │ │ │ ├── mode-pgsql.js │ │ │ │ ├── mode-php.js │ │ │ │ ├── mode-plain_text.js │ │ │ │ ├── mode-powershell.js │ │ │ │ ├── mode-praat.js │ │ │ │ ├── mode-prolog.js │ │ │ │ ├── mode-properties.js │ │ │ │ ├── mode-protobuf.js │ │ │ │ ├── mode-python.js │ │ │ │ ├── mode-r.js │ │ │ │ ├── mode-rdoc.js │ │ │ │ ├── mode-rhtml.js │ │ │ │ ├── mode-ruby.js │ │ │ │ ├── mode-rust.js │ │ │ │ ├── mode-sass.js │ │ │ │ ├── mode-scad.js │ │ │ │ ├── mode-scala.js │ │ │ │ ├── mode-scheme.js │ │ │ │ ├── mode-scss.js │ │ │ │ ├── mode-sh.js │ │ │ │ ├── mode-sjs.js │ │ │ │ ├── mode-smarty.js │ │ │ │ ├── mode-snippets.js │ │ │ │ ├── mode-soy_template.js │ │ │ │ ├── mode-space.js │ │ │ │ ├── mode-sql.js │ │ │ │ ├── mode-sqlserver.js │ │ │ │ ├── mode-stylus.js │ │ │ │ ├── mode-svg.js │ │ │ │ ├── mode-swift.js │ │ │ │ ├── mode-swig.js │ │ │ │ ├── mode-tcl.js │ │ │ │ ├── mode-tex.js │ │ │ │ ├── mode-text.js │ │ │ │ ├── mode-textile.js │ │ │ │ ├── mode-toml.js │ │ │ │ ├── mode-twig.js │ │ │ │ ├── mode-typescript.js │ │ │ │ ├── mode-vala.js │ │ │ │ ├── mode-vbscript.js │ │ │ │ ├── mode-velocity.js │ │ │ │ ├── mode-verilog.js │ │ │ │ ├── mode-vhdl.js │ │ │ │ ├── mode-xml.js │ │ │ │ ├── mode-xquery.js │ │ │ │ ├── mode-yaml.js │ │ │ │ ├── snippets/ │ │ │ │ │ ├── abap.js │ │ │ │ │ ├── abc.js │ │ │ │ │ ├── actionscript.js │ │ │ │ │ ├── ada.js │ │ │ │ │ ├── apache_conf.js │ │ │ │ │ ├── applescript.js │ │ │ │ │ ├── asciidoc.js │ │ │ │ │ ├── assembly_x86.js │ │ │ │ │ ├── autohotkey.js │ │ │ │ │ ├── batchfile.js │ │ │ │ │ ├── c9search.js │ │ │ │ │ ├── c_cpp.js │ │ │ │ │ ├── cirru.js │ │ │ │ │ ├── clojure.js │ │ │ │ │ ├── cobol.js │ │ │ │ │ ├── coffee.js │ │ │ │ │ ├── coldfusion.js │ │ │ │ │ ├── csharp.js │ │ │ │ │ ├── css.js │ │ │ │ │ ├── curly.js │ │ │ │ │ ├── d.js │ │ │ │ │ ├── dart.js │ │ │ │ │ ├── diff.js │ │ │ │ │ ├── django.js │ │ │ │ │ ├── dockerfile.js │ │ │ │ │ ├── dot.js │ │ │ │ │ ├── eiffel.js │ │ │ │ │ ├── ejs.js │ │ │ │ │ ├── elixir.js │ │ │ │ │ ├── elm.js │ │ │ │ │ ├── erlang.js │ │ │ │ │ ├── forth.js │ │ │ │ │ ├── ftl.js │ │ │ │ │ ├── gcode.js │ │ │ │ │ ├── gherkin.js │ │ │ │ │ ├── gitignore.js │ │ │ │ │ ├── glsl.js │ │ │ │ │ ├── golang.js │ │ │ │ │ ├── groovy.js │ │ │ │ │ ├── haml.js │ │ │ │ │ ├── handlebars.js │ │ │ │ │ ├── haskell.js │ │ │ │ │ ├── haxe.js │ │ │ │ │ ├── html.js │ │ │ │ │ ├── html_elixir.js │ │ │ │ │ ├── html_ruby.js │ │ │ │ │ ├── ini.js │ │ │ │ │ ├── io.js │ │ │ │ │ ├── jack.js │ │ │ │ │ ├── jade.js │ │ │ │ │ ├── java.js │ │ │ │ │ ├── javascript.js │ │ │ │ │ ├── json.js │ │ │ │ │ ├── jsoniq.js │ │ │ │ │ ├── jsp.js │ │ │ │ │ ├── jsx.js │ │ │ │ │ ├── julia.js │ │ │ │ │ ├── latex.js │ │ │ │ │ ├── lean.js │ │ │ │ │ ├── less.js │ │ │ │ │ ├── liquid.js │ │ │ │ │ ├── lisp.js │ │ │ │ │ ├── live_script.js │ │ │ │ │ ├── livescript.js │ │ │ │ │ ├── logiql.js │ │ │ │ │ ├── lsl.js │ │ │ │ │ ├── lua.js │ │ │ │ │ ├── luapage.js │ │ │ │ │ ├── lucene.js │ │ │ │ │ ├── makefile.js │ │ │ │ │ ├── markdown.js │ │ │ │ │ ├── mask.js │ │ │ │ │ ├── matlab.js │ │ │ │ │ ├── maze.js │ │ │ │ │ ├── mel.js │ │ │ │ │ ├── mips_assembler.js │ │ │ │ │ ├── mipsassembler.js │ │ │ │ │ ├── mushcode.js │ │ │ │ │ ├── mysql.js │ │ │ │ │ ├── nix.js │ │ │ │ │ ├── objectivec.js │ │ │ │ │ ├── ocaml.js │ │ │ │ │ ├── pascal.js │ │ │ │ │ ├── perl.js │ │ │ │ │ ├── pgsql.js │ │ │ │ │ ├── php.js │ │ │ │ │ ├── plain_text.js │ │ │ │ │ ├── powershell.js │ │ │ │ │ ├── praat.js │ │ │ │ │ ├── prolog.js │ │ │ │ │ ├── properties.js │ │ │ │ │ ├── protobuf.js │ │ │ │ │ ├── python.js │ │ │ │ │ ├── r.js │ │ │ │ │ ├── rdoc.js │ │ │ │ │ ├── rhtml.js │ │ │ │ │ ├── ruby.js │ │ │ │ │ ├── rust.js │ │ │ │ │ ├── sass.js │ │ │ │ │ ├── scad.js │ │ │ │ │ ├── scala.js │ │ │ │ │ ├── scheme.js │ │ │ │ │ ├── scss.js │ │ │ │ │ ├── sh.js │ │ │ │ │ ├── sjs.js │ │ │ │ │ ├── smarty.js │ │ │ │ │ ├── snippets.js │ │ │ │ │ ├── soy_template.js │ │ │ │ │ ├── space.js │ │ │ │ │ ├── sql.js │ │ │ │ │ ├── sqlserver.js │ │ │ │ │ ├── stylus.js │ │ │ │ │ ├── svg.js │ │ │ │ │ ├── swift.js │ │ │ │ │ ├── swig.js │ │ │ │ │ ├── tcl.js │ │ │ │ │ ├── tex.js │ │ │ │ │ ├── text.js │ │ │ │ │ ├── textile.js │ │ │ │ │ ├── toml.js │ │ │ │ │ ├── twig.js │ │ │ │ │ ├── typescript.js │ │ │ │ │ ├── vala.js │ │ │ │ │ ├── vbscript.js │ │ │ │ │ ├── velocity.js │ │ │ │ │ ├── verilog.js │ │ │ │ │ ├── vhdl.js │ │ │ │ │ ├── xml.js │ │ │ │ │ ├── xquery.js │ │ │ │ │ └── yaml.js │ │ │ │ ├── theme-ambiance.js │ │ │ │ ├── theme-chaos.js │ │ │ │ ├── theme-chrome.js │ │ │ │ ├── theme-clouds.js │ │ │ │ ├── theme-clouds_midnight.js │ │ │ │ ├── theme-cobalt.js │ │ │ │ ├── theme-crimson_editor.js │ │ │ │ ├── theme-dawn.js │ │ │ │ ├── theme-dreamweaver.js │ │ │ │ ├── theme-eclipse.js │ │ │ │ ├── theme-github.js │ │ │ │ ├── theme-idle_fingers.js │ │ │ │ ├── theme-iplastic.js │ │ │ │ ├── theme-katzenmilch.js │ │ │ │ ├── theme-kr_theme.js │ │ │ │ ├── theme-kuroir.js │ │ │ │ ├── theme-merbivore.js │ │ │ │ ├── theme-merbivore_soft.js │ │ │ │ ├── theme-mono_industrial.js │ │ │ │ ├── theme-monokai.js │ │ │ │ ├── theme-pastel_on_dark.js │ │ │ │ ├── theme-solarized_dark.js │ │ │ │ ├── theme-solarized_light.js │ │ │ │ ├── theme-sqlserver.js │ │ │ │ ├── theme-terminal.js │ │ │ │ ├── theme-textmate.js │ │ │ │ ├── theme-tomorrow.js │ │ │ │ ├── theme-tomorrow_night.js │ │ │ │ ├── theme-tomorrow_night_blue.js │ │ │ │ ├── theme-tomorrow_night_bright.js │ │ │ │ ├── theme-tomorrow_night_eighties.js │ │ │ │ ├── theme-twilight.js │ │ │ │ ├── theme-vibrant_ink.js │ │ │ │ ├── theme-xcode.js │ │ │ │ ├── worker-coffee.js │ │ │ │ ├── worker-css.js │ │ │ │ ├── worker-html.js │ │ │ │ ├── worker-javascript.js │ │ │ │ ├── worker-json.js │ │ │ │ ├── worker-lua.js │ │ │ │ ├── worker-php.js │ │ │ │ ├── worker-xml.js │ │ │ │ └── worker-xquery.js │ │ │ ├── ace.css │ │ │ ├── ace.js │ │ │ └── pdf.css │ │ ├── gitbook-plugin-advanced-emoji/ │ │ │ ├── LICENSE-IMAGES.md │ │ │ ├── LICENSE.md │ │ │ ├── emoji-book.css │ │ │ └── emoji-website.css │ │ ├── gitbook-plugin-anchor-navigation-ex/ │ │ │ ├── lib/ │ │ │ │ ├── config.js │ │ │ │ ├── log.js │ │ │ │ └── plugin.js │ │ │ └── style/ │ │ │ └── plugin.css │ │ ├── gitbook-plugin-anchors/ │ │ │ └── plugin.css │ │ ├── gitbook-plugin-chart/ │ │ │ └── highcharts/ │ │ │ └── highcharts.js │ │ ├── gitbook-plugin-disqus/ │ │ │ ├── plugin.css │ │ │ └── plugin.js │ │ ├── gitbook-plugin-edit-link/ │ │ │ └── plugin.js │ │ ├── gitbook-plugin-emphasize/ │ │ │ └── plugin.css │ │ ├── gitbook-plugin-expandable-chapters-small/ │ │ │ ├── expandable-chapters-small.css │ │ │ └── expandable-chapters-small.js │ │ ├── gitbook-plugin-fontsettings/ │ │ │ ├── fontsettings.js │ │ │ └── website.css │ │ ├── gitbook-plugin-github/ │ │ │ └── plugin.js │ │ ├── gitbook-plugin-page-footer-ex/ │ │ │ ├── lib/ │ │ │ │ └── plugin.js │ │ │ └── style/ │ │ │ └── plugin.css │ │ ├── gitbook-plugin-prism/ │ │ │ ├── prism-coy.css │ │ │ ├── prism-dark.css │ │ │ ├── prism-funky.css │ │ │ ├── prism-okaidia.css │ │ │ ├── prism-solarizedlight.css │ │ │ ├── prism-tomorrow.css │ │ │ ├── prism-twilight.css │ │ │ └── prism.css │ │ ├── gitbook-plugin-search-plus/ │ │ │ ├── search.css │ │ │ └── search.js │ │ ├── gitbook-plugin-sectionx/ │ │ │ ├── sectionx.css │ │ │ └── sectionx.js │ │ ├── gitbook-plugin-sharing-plus/ │ │ │ └── buttons.js │ │ ├── gitbook-plugin-splitter/ │ │ │ ├── splitter.css │ │ │ └── splitter.js │ │ ├── gitbook.js │ │ ├── style.css │ │ └── theme.js │ ├── index.html │ ├── package.json │ ├── scripts/ │ │ └── build.sh │ ├── search_plus_index.json │ └── thanks.html ├── package.json ├── scripts/ │ └── build.sh └── thanks.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] charset = utf-8 indent_style = space indent_size = 2 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true ================================================ FILE: .gitignore ================================================ # Referenced from https://github.com/github/gitignore/blob/master/Node.gitignore _book # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Typescript v1 declaration files typings/ # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env # next.js build output .next # other stuff .DS_Store Thumbs.db # IDE configurations .idea .vscode # build assets /output /dist /dll ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================ # 《PWA 应用实战》 欢迎走进 PWA 世界!! ## 简介 本书围绕着 PWA 以及周边技术,从概念入手,以实战的方式给读者讲述如何编写 PWA,以及如何编写体验最好、速度最快、安全的 PWA 站点。 本书主要从以下几个部分讲述 PWA。 - 设计与体验 - 基础技术 - Service Worker - 离线与缓存 - 用户留存 - 安全 - 性能 ## 关于作者 作者:**百度 Web 生态团队** 百度 Web 生态团队是 2017 年组建,旨在帮助国内 Web 的发展,提升 Web 的用户体验,目前已有多个产品,如 [LAVAS](https://lavas.baidu.com) 和 [MIP](https://www.mipengine.org)。 ## 为什么写这本书 我们团队从成立到现在,已有 2 年,推出 LAVAS 和 MIP 也是我们的尝试之一,积极参与 Web 方向上的技术讨论,也经常去国内的技术会议上进行分享,虽不能说真给国内 Web 发展带来了多大的贡献,但我们至少做了很多尝试。 因此,我们希望将我们过去两年积累的 PWA 方面的经验写下来,不仅帮我们自己梳理知识结构,也希望这本书能帮助到正在读的您。 ## 为什么开源 PWA 它还在不断进步,本书的内容随时需要更新,电子书对于我们来说,迭代起来很方便。 同时也欢迎对 Web 和 PWA 有浓厚兴趣的读者加入我们,一起来维护这本书,我们由衷感谢。 ## 版权 本书为开源书,读者可以自由地分享。只要遵守惟下列条件: - **署名** — 您必须给出适当的署名,提供指向本许可协议的链接,同时标明是否(对原始作品)作了修改。您可以用任何合理的方式来署名,但是不得以任何方式暗示许可人为您或您的使用背书。 - **非商业性使用** — 您不得将本作品用于商业目的。 - **禁止演绎** — 如您再混合、转换、或者基于该书的创作,您不可以分发修改作品。 - **禁止附加限制** — 您不得使用法律术语或采用技术措施限制其他人做许可协议允许的事情。 本书采用 [知识共享署名-非商业性使用 4.0 国际许可协议](https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode) 进行许可 ## 最后 本书还在勘误阶段,可能会存在一定的问题,请您在分享的过程一定要注明来源,帮助所有读者溯源,让他们能够获得最新的修改内容版本。如果您在阅读的过程发现了书中的问题,欢迎提交 [Issue](https://github.com/lavas-project/pwa-book/issues) 反馈给我们,或者直接发送 [Pull Request](https://github.com/lavas-project/pwa-book/pulls) 帮助我们修正,感谢您对本书的支持和贡献! ================================================ FILE: SUMMARY.md ================================================ # Summary * [HOME](README.md) * [第1章 PWA 介绍](chapter01.md) * [1.1 为什么会出现 PWA](chapter01/1-how-was-pwa-born.md) * [1.2 什么是 PWA](chapter01/2-what-is-pwa.md) * [1.3 PWA 的核心技术](chapter01/3-what-are-key-techs.md) * [1.4 PWA 的发展](chapter01/4-how-is-pwa-going.md) * [1.5 PWA 的未来](chapter01/5-the-future-of-pwa.md) * [1.6 你的第一个 PWA](chapter01/6-your-first-pwa.md) * [第2章 设计与体验](chapter02.md) * [2.1 什么是好的用户体验](chapter02/1-what-is-good-ux.md) * [2.2 设计与技术](chapter02/2-design-and-tech.md) * [2.3 App Shell](chapter02/3-app-shell.md) * [2.4 骨架屏](chapter02/4-app-skeleton.md) * [2.5 响应式布局](chapter02/5-responsive-design.md) * [第3章 基础技术简介](chapter03.md) * [3.1 Promise](chapter03/1-promise.md) * [3.2 Async 函数](chapter03/2-async-function.md) * [3.3 Fetch API](chapter03/3-fetch-api.md) * [3.4 Cache API](chapter03/4-cache-api.md) * [3.5 IndexedDB](chapter03/5-indexeddb.md) * [第4章 Service Worker](chapter04.md) * [4.1 Service Worker 简介](chapter04/1-service-worker-introduction.md) * [4.2 Service Worker 注册](chapter04/2-service-worker-register.md) * [4.3 Service Worker 工作原理](chapter04/3-service-worker-dive.md) * [4.4 Service Worker 调试](chapter04/4-service-worker-debug.md) * [第5章 离线与缓存](chapter05.md) * [5.1 资源请求的拦截代理](chapter05/1-fetch-event-management.md) * [5.2 本地存储管理](chapter05/2-local-storage-management.md) * [5.3 资源请求响应策略](chapter05/3-respond-strategy.md) * [5.4 预缓存方案](chapter05/4-precache.md) * [5.5 使用 Workbox](chapter05/5-workbox.md) * [第6章 用户留存](chapter06.md) * [6.1 Web 应用清单](chapter06/1-manifest-json.md) * [6.2 凭证管理 API](chapter06/2-credentials-api.md) * [6.3 桌面通知](chapter06/3-notification-api.md) * [6.4 网络推送](chapter06/4-web-push-api.md) * [6.5 Payment Request API](chapter06/5-payment-request-api.md) * [第7章 安全](chapter07.md) * [7.1 使用 HTTPS 保护站点安全](chapter07/1-https.md) * [7.2 内容安全策略](chapter07/2-CSP.md) * [7.3 同源策略](chapter07/3-policy.md) * [7.4 典型的安全漏洞](chapter07/4-vulnerability.md) * [第8章 性能](chapter08.md) * [8.1 加载性能 ](chapter08/1-loading-performance.md) * [8.2 渲染性能 ](chapter08/2-rendering-performance.md) * [第9章 PWA 与搜索](chapter09.md) * [9.1 搜索引擎收录](chapter09/1-search-engine-index.md) * [9.2 PWA 与 AMP/MIP](chapter09/2-pwa-and-amp-and-mip.md) * [9.3 全站 AMP/MIP](chapter09/3-whole-site-amp-and-mip.md) * [9.4 AMP/MIP 预加载 PWA](chapter09/4-preload-pwa.md) * [附录1 使用 Lighthouse 测评 PWA](appendix01.md) * [1 使用 Lighthouse 测评 PWA](appendix01/1-lighthouse.md) * [2 Lighthouse 评分指南](appendix01/2-lighthouse-score-guide.md) * [3 Lighthouse 使用案例](appendix01/3-lighthouse-case.md) * [感谢](thanks.md) ================================================ FILE: appendix01/1-lighthouse.md ================================================ ## 使用 Lighthouse 测评 PWA Lighthouse 是 Google Chrome 推出的一个开源自动化工具,能够对 PWA 多方面的效果指标进行评测,并给出最佳实践的建议以帮助开发者改进 PWA 的质量。它的使用方法也非常简单,我们只需要提供一个要测评的网址,它将针对此页面运行一系列的测试,然后生成一个有关页面性能的报告。通过报告我们就可以知道需要采取哪些措施来改进应用的性能和体验。 ### 使用入门 针对不同的适用场景,我们可以通过多种方式来安装并使用 Lighthouse: - Chrome 浏览器插件。Chrome 插件的形式提供了更加友好的用户界面,方便读取报告。 - Chrome DevTools。该工具集成在最新版本的 Chrome 浏览器中,无需安装即可使用。 - Lighthouse CLI 命令行工具。方便将 Lighthouse 集成到持续集成系统中。 - 编程的方式。我们也能通过 Node.js 模块引入 Lighthouse 工具包,以编程的形式来使用它。 下面我们依次介绍上述这几种使用方法。 ### Chrome 插件 下载 Google Chrome 52 或更高版本,并安装 [Ligthouse Chrome 插件](https://chrome.google.com/webstore/detail/lighthouse/blipmdconlkpinefehnmjammfjpmpbjk)。打开需要进行测评的页面,并点击 Chrome 工具栏上的 Lighthouse 图标如下图: ![icon](./img/icon.png) 如果在工具栏上没有看到此图标,它可能隐藏在 Chrome 的主菜单中。点击此图标后,我们会看到一个展开菜单,如下图 ![menu](./img/chrome_extensions.png) 在 `Options` 选项里,可以选择需要的测评项,包括性能、无障碍访问性、最佳实践、SEO、PWA,默认情况下,选择全部即可。 ![menu](./img/ext_panel.png) 点击 Generate report 按钮以针对当前打开的页面运行 Lighthouse 测试。 在完成测评后,Lighthouse 将打开一个新标签,并在页面的结果上显示一个报告。 ![report](./img/report_result.png) 在这里,我们就能看到关于 PWA, Performance, Accessibility, Best Practices 四个方面存在的问题以及相关建议。根据这些去优化你的站点吧! ### Chrome DevTools 在最新版本的 Chrome 浏览器中,Lighthouse 已经直接集成到了调试工具 DevTools 中了,因此不需要进行任何安装或下载。我们先打开需要进行测试的页面,打开 Chrome DevTools,选择 `Audits` 面板,就能看到 Lighthouse 工具的一些配置选项,选择需要的配置后,点击 `Run audits`,工具就会对当前页面进行性能的测评。 ![menu](./img/dev_tool.png) ### 命令行工具(Lighthouse CLI) 上述两种工具提供了较为基础、快捷的使用方式,针对进阶的高级开发者,Lighthouse 的命令行工具更为合适。它提供了多种详细的使用参数,通过参数的配置,能够更灵活地进行测评并输出报告结果。此外,在一些自动化的持续集成场景中,命令行工具显得十分有用。 #### 安装 安装 [Node](https://nodejs.org/zh-cn/download/),需要 Node 8 LTS(8.9) 及以上版本。 以全局方式安装: ```shell npm install -g lighthouse # or use yarn: # yarn global add lighthouse ``` #### 运行 CLI 针对一个页面运行 `lighthouse` 命令,进行测评: ```shell lighthouse https://www.example.com/ ``` > 注意: > CLI 相较于插件和 DevTools 存在一些尚未修复的问题,比如对 Ubuntu 系统支持不好,会抛 NO_FCP 的错误。对于某些网站支持不好,这些网站可能存在以下特征导致 NO_FCP 错误的发生: > 1. 初始渲染的时候页面不包含文本,而只包含一个 spinner > 2. 你的站点的启动和渲染主要内容在 5-10s 的延迟之后发生 > [点击](https://github.com/GoogleChrome/lighthouse/issues/6154)查看最新解决方案 默认情况下,运行报告将以 html 的格式输出在当前目录的 `./_.report.html` 文件中,如上述命令结果将存储在 www.example.com_2019-04-02_16-51-15.report.html。我们也可以指定输出的格式与路径,如: ```shell lighthouse https://example.com/ --output html --output-path ./report.html ``` 如果需要将结果在标准输出流中以 json 格式输出: ```shell lighthouse https://example.com/ --output json ``` 将结果以 json 文件的形式输出到本地目录: ```shell lighthouse https://example.com/ --output json --output-path ./myfile.json ``` 输入 `--help` 选项可以查看可用的输入、输出选项 ```shell lighthouse --help ``` #### 生命周期 Lighthouse 运行测评的过程有一套完整的生命周期,可以划分成三个主要流程:首先是 Collecting 流程,这一步会调用内置的 Driver,其作用是通过 Chrome DevTools Protocol 调起浏览器,并创建新的 tab 请求待测评的站点,通过浏览器采集站点数据并将结果(称之为 Artifacts)保存在本地临时目录。然后进入 Auditing 流程,读取 Artifacts 数据,根据内置的评判策略逐条进行检查并计算出各项的数字形式得分。最后进行 Report 流程,将评分结果按照 PWA、性能、无障碍访问、最佳实践等纬度进行划分,以 JSON、HTML 等格式输出。如下图: ![lifecircle](./img/life_circle.jpg) 命令行工具基于此提供了生命周期的选项,我们可以让 CLI 只运行整个测评过程的一个或多个特定生命周期。比如,使用 --gather-mode(-G)只进行资源采集的生命周期,命令行工具将会启动浏览器,采集被测试站点的相关数据,并将结果以 json 的形式存储到本地,默认是 `./latest-run/` 目录,然后退出进程: ```shell lighthouse https://example.com/ -G ``` 如果想要跳过浏览器的交互,直接从本地读取页面的临时数据,运行测评和产出结果报告,则可以使用 --audit-mode(-A),默认将从 `./latest-run/` 目录读取: ```shell lighthouse https://example.com/ -A ``` 两个选项同时使用,就会运行整个测评的生命周期,与直接运行 lighthouse 命令相比,会在当前目录保存一份测试站点的数据。 ```shell lighthouse https://example.com -GA ``` 如果不想使用默认的 `./latest-run/` 目录,我们也能自定义站点的 json 数据的保存目录,如: ``` lighthouse -GA=./mycustomfolder https://example.com ``` ### 编程的方式使用 Lighthouse 除了上述几种方式之外,Lighthouse 也提供了 NPM 包,我们能够以 Node.js 模块的形式引入到项目代码中,结合另一个 NPM 模块 `chrome-launcher` 用于调起浏览器,就可以随心所欲地以编程的方式使用了。 首先要在项目中安装依赖模块: ```shell npm install lighthouse chrome-launcher # yarn add lighthouse chrome-launcher ``` 在使用 lighthouse 接口之前,需要先用启动一个 chrome,所以我们可以封装一个名为 launchChromeAndRunLighthouse 的方法,来完成这两步。chromeLauncher 工具包提供了 launch 方法,接收一个启动所需的参数 opts,这个参数用于配置 chrome 的启动环境、启动方式等,在返回的 promise 对象中,我们能获取到 chrome 实例。再将待测试 url 及 opts 提供给 lighthouse,测试完成后需要关掉 chrome 实例,然后就能拿到我们需要的测试报告 results 了。 ```javascript const lighthouse = require('lighthouse') const chromeLauncher = require('chrome-launcher') function launchChromeAndRunLighthouse(url, opts, config = null) { return chromeLauncher.launch({chromeFlags: opts.chromeFlags}).then(chrome => { opts.port = chrome.port return lighthouse(url, opts, config).then(results => { return chrome.kill().then(() => results) }) }) } const opts = { chromeFlags: ['--show-paint-rects'] } launchChromeAndRunLighthouse('https://example.com', opts).then(results => { // Use results! }) ``` 值得一提的是这里的 result 包含了多种类型的数据格式,如 `results.lhr` 是 javascript 对象(Lighthouse Result Object),而 `results.report` 则是用于 HTML/JSON/CSV 输出结果的字符串,可以按需选择。 #### 性能报告 对于一些只关注应用性能的使用场景,可以用 onlyCategories 配置项来设定只运行性能这个维度的测试: ```javascript // ... const flags = {onlyCategories: ['performance']} launchChromeAndRunLighthouse(url, flags).then(results => { // Use performance results! }) ``` 除此之外,我们可以自定义自己需要的测评维度,Lighthouse 的 [Github 仓库](https://github.com/GoogleChrome/lighthouse/tree/master/docs/recipes/custom-audit)提供了一些自定义的示例和方法。 注意,用编程的方式使用 lighthouse 时,在上述代码的 flags 参数的使用上,与 CLI 的方式有所区别。考虑到以 node 模块引入的情况下,开发者自己来控制这些选项更为简单和灵活,部分 flag 选项值只在 CLI 的模式下生效。node 模块和 CLI 中都能使用的选项值,可以参考 [官方的定义](https://github.com/GoogleChrome/lighthouse/blob/888bd6dc9d927a734a8e20ea8a0248baa5b425ed/typings/externs.d.ts#L82-L119)。 #### 开启测试日志 如果运行期间需要查看测试的日志,可以引入 lighthouse-logger 模块,它支持打印不同级别的日志。用法如下: ```javascript const log = require('lighthouse-logger') const flags = {logLevel: 'info'} log.setLevel(flags.logLevel) launchChromeAndRunLighthouse('https://example.com', flags).then(...) ``` ### 自定义配置 你可能已经注意到 lighthouse 在调用的时候还有第三个参数:`lighthouse(url, opts, config)`,这个 config 参数能够帮助我们用更细粒度的控制整个测评。这种细粒度的配置在 CLI 中也是支持的,只是使用方式略有区别: 我们可以指定一个配置文件 custom-config.js 如下,它在继承默认的配置的基础上进行了一定的自定义: ```javascript module.exports = { extends: 'lighthouse:default', settings: { onlyAudits: [ 'first-meaningful-paint', 'speed-index-metric', 'estimated-input-latency', 'first-interactive', 'consistently-interactive', ], }, } ``` 如果使用 CLI,需要用 --config-path 指向该配置文件: ```bash lighthouse --config-path=path/to/custom-config.js https://example.com ``` 对于 Node 模块的形式,也基本相似,将配置对象当做 lighthouse 方法的第三个参数传入即可: ```javascript const lighthouse = require('lighthouse') const config = require('./path/to/custom-config.js') lighthouse('https://example.com/', {port: 9222}, config) ``` 如果评测过程中有这种比较高阶的配置需求,可以详细了解相关的[属性文档](https://github.com/GoogleChrome/lighthouse/blob/master/docs/configuration.md),这里我们就不作展开了。 除了这些官方提供的工具之外,一些开源项目也将 Lighthouse 进行了集成。例如使用 Webpack 构建的应用,可以使用 `webpack-lighthouse-plugin`,在构建的过程中完成 Lighthouse 测评;`lighthouse-mocha-example` 则能够在 Mocha 的测试用例中增加 Lighthouse 的测评项等等,整个工具生态日渐繁荣,相信对于我们测评 PWA 性能,构建更完美的 PWA 大有裨益。 ================================================ FILE: appendix01/2-lighthouse-score-guide.md ================================================ ## Lighthouse 评分指南 使用 Lighthouse 对网站进行测评后,我们会得到一份评分报告,它包含了性能(Performance),PWA(Progressive Web App),访问无障碍(Accessibility),最佳实践(Best Practice),搜索引擎优化(SEO)等几个部分。这里我们将介绍一下 Lighthouse 是如何计算这几个评分的。 ![score](./img/lavas_report.png) ### 性能评分 性能评分的分值区间是0到100,如果出现0分,通常是在运行 Lighthouse 时发生了错误,如果反复出现了0分的情况,可以在 Lighthouse 的 GitHub 仓库去[反馈 bug](https://github.com/GoogleChrome/lighthouse/issues/new)。满分100分代表了站点已经达到了98分位值的数据,而50分则是75分位值的数据。 #### 影响评分的性能指标 性能测试指标分成了 Metrics,Diagnostic,Opportunities 三部分。通常情况下,只有 Metrics 部分的指标项会对分数产生直接影响,Lighthouse 会衡量以下性能指标项: - 首次内容绘制(First Contentful Paint)。即浏览器首次将任意内容(如文字、图像、canvas 等)绘制到屏幕上的时间点。 - 首次有效绘制(First Meaningful Paint)。衡量了用户感知页面的主要内容(primary content)可见的时间。对于不同的站点,首要内容是不同的,例如:对于博客文章,标题及首屏文字是首要内容,而对于购物网站来说,图片也会变得很重要。 - 首次 CPU 空闲(First CPU Idle)。即页面首次能够对输入做出反应的时间点,其出现时机往往在首次有效绘制完成之后。该指标目前仍处于实验阶段。 - 可交互时间(Time to Interactive)。指的是所有的页面内容都已经成功加载,且能够快速地对用户的操作做出反应的时间点。该指标目前仍处于实验阶段。 - 速度指标(Speed Index)。衡量了首屏可见内容绘制在屏幕上的速度。在首次加载页面的过程中尽量展现更多的内容,往往能给用户带来更好的体验,所以速度指标的值约小越好。 - 输入延迟估值(Estimated Input Latency)。这个指标衡量了页面对用户输入行为的反应速度,其基准值应低于 50ms。 #### 性能指标评分的计算 每一项性能指标对评分的贡献都有其计算逻辑,Lighthouse 会将原始的性能值映射成为 0-100 之间的数字。评分使用 HTTPArchive 上的真实站点性能数据作为样本,统计出对数正态分布。例如,FMP(First Meaningful Paint)的原始值是从页面初始化开始到主要内容渲染成功的耗时,根据真实站点的数据,顶级性能的站点的 FMP 值约为 1220ms,这个值会被映射成 Lighthouse 的 99 分。 针对不同的评分,Lighthouse 用了不同的颜色进行标注,分值区间和颜色的对应关系如下: - 0 - 49(慢):红色 - 50 - 89(平均值): 橙色 - 90 - 100(快): 绿色 #### 性能评分的分配权重 各个指标对性能评分的贡献并不相同,Lighthouse 提供了[评分详情表](https://docs.google.com/spreadsheets/d/1Cxzhy5ecqJCucdf1M0iOzM8mIxNc7mmx107o5nj38Eo/edit#gid=0),形式如下图,来查阅具体权重分配情况。权重较大的指标,对性能评分的影响更大一些,最终的总体性能评分是这些性能指标分数的加权平均值。我们可以用这个分数计算的[表格](https://docs.google.com/spreadsheets/d/1dXH-bXX3gxqqpD1f7rp6ImSOhobsT1gn_GQ2fGZp8UU/edit?ts=59fb61d2#gid=283330180)作为参考,以了解不同的指标是如何影响最终评分的。 ![table](./img/table.png) 从表中可以看出,性能的指标项权重分配如下: - 3X - 首次内容绘制 - 1X - 首次有效绘制 - 2X - 首次 CPU 空闲 - 5X - 可交互时间 - 4X - 速度指标 - 0X - 输入延迟估值 这些权重的分配方式仍在探究之中,Lighthouse 的团队也在努力试图收集更多站点的数据样本,来将这套计算方法规范化。 #### 减少性能评分的波动 当我们使用 Lighthouse 对实际站点进行测试时,难以避免会有一些影响评分的变量存在。每次访问,站点可能会加载不同的广告、脚本等,网络状况也可能不同。杀毒软件、浏览器插件以及一些其他对页面加载有干预的程序都可能导致评分的较大波动。所以,我们应尽量避免上述因素的影响,以得到更加一致性的数据。可以考虑使用持续集成系统或者第三方服务(如 [WebPageTest](https://webpagetest.org/easy))进行测试,往往会有更准确的数据。 #### 提高性能评分 前面提到性能评分分为了三部分,Metrics 部分的指标项会直接影响分数,可以作为我们的主要参考点。另外的两部分中, Opportunities 提供了详细的建议和文档,来解释低分的原因,帮助我们具体进行实现和改进。Diagnostics 部分的列表项则为进一步改善性能的实验和调整,给出了指导。这两者不会纳入分数的计算。 ### PWA 评分 #### 基准指标项 PWA(Progressive Web App)评分的分值区间也是0-100。Lighthouse 使用 PWA 基准检查项列表(Baseline PWA Checklist)进行测评,测评结果将这些指标项分成了四个类别,共包含12个自动测试项和3个手动测试项,其中各个自动测试项的评分权重是相同的。PWA 的评测指标对我们来说非常重要,我们可以从这四个类别详细了解一下基准指标项。 ##### 快速可靠 1. 页面在移动网络条件下能够快速加载。 2. 在离线条件下页面能够返回状态码200。这里我们可以通过 Service Worker 来实现离线可用。 3. start_url 在离线条件下返回状态码200。start_url 是前面章节我们提到过的 manifest.json 中的一个属性,它指定了用户打开该 PWA 时加载的 URL。 ##### 可安装 1. 始终使用 HTTPS。 2. 注册 Service Worker 来缓存页面以及 start_url。 3. 使用 manifest 文件来实现安装 PWA 的需求,浏览器能够主动通知用户将应用添加到桌面,增加留存率。 ##### PWA 优化 1. 将 HTTP 流量重定向到 HTTPS。 2. 配置自定义启动画面。 3. 设置地址栏主题颜色。 4. 页面内容针对视口大小自适应,对移动用户的展示更友好。 5. 使用了 `` 标签,并设置了 width 或 initial-scale 属性。 6. 当 JavaScript 文件不可用时,提供降级措施,页面能显示基本内容而不出现白屏。 ##### 手动测试项 1. 站点跨浏览器可用,如主流浏览器 Chrome, Edge, Firefox 及 Safari 等。 2. 页面间切换流畅,即使在较差的网络环境下,切换动画也应该简洁顺畅,这是提高用户感知体验的关键。 3. 保证每个页面都有独一无二的 URL,能够在新的浏览器窗口打开,且方便在社交媒体上进行分享。 #### 示范性指标项 除了上述基准指标项之外,为了让 PWA 的体验更加完美,还有 Lighthouse 未实现检查的进阶指标,也就是可以作为示范性参考的 PWA 的指标,这些指标大多需要人工进行确认,它们也分成了几个类别,我们可以针对性地改进 PWA。 ##### 可索引 & 社交支持 1. 站点的内容可以被 Baidu,Google 等搜索引擎收录索引。 2. 提供必要的 `Schema.org` 标记数据,搜索引擎都会依据这些标记来优化搜索结果,让人们更容易找到正确的网页。 3. 提供必要的社交标记数据,如 Twitter 的 `` 标签,能够便于被这些社交站点的爬虫抓取。 4. 提供 Canonical URL,一般适用于页面内容能使用多个 URL 访问的场景。 5. 页面使用 History API,对于单页应用,确保 URL 不要使用片段标识符,如 `https://example.com/#!user/26601` 中 #!后面的部分。 ##### 用户体验 1. 当页面加载时,确保页面内容不跳动或闪烁。 2. 从详情页返回列表页时,记录之前的滚动位置。 3. 输入文字时,键盘不会挡住输入框。 4. 在 Standalone 模式下(从主屏幕启动的情况),页面内容可分享。 5. 在不同的移动设备及桌面电脑上,站点能够自适应。 6. 不要过度使用安装 APP 的推送。 7. 在适当的时候显示添加到桌面的推送,避免打断用户正常使用流程。 ##### 性能 1. 首次加载速度尽可能快,即使是在慢速 3G 移动网络下,可交互时间也应该在 5s 以内(在基准指标中,这个时间是 10s)。 ##### 缓存 1. 站点使用 cache-first (缓存优先)的缓存策略。尽可能地使用这种策略,确保在慢网络环境和离线环境的加载速度始终相当。 2. 用户处于离线状态时,应当适当地给出通知。可以使用 Network Information API 来实现。 ##### 推送通知 1. 告知用户通知功能的使用背景和原因,保证得到许可。 2. 建议用户开启推送通知的 UI 应当尽量友好,不能太强势。 3. 站点显示权限请求时,屏幕的其他主体内容应当适度变暗,让用户更清楚地看到站点需要开启通知权限的理由。 4. 推送通知的内容应当及时、准确,且具有较强的关联性。 5. 给用户提供开启、关闭通知的操作权限。 ##### 附加功能点 1. 使用 Credential Management API,帮助用户实现跨设备登录。 2. 使用 Payment Request API,帮助用户方便地调起更友好的原生界面(native UI)进行支付操作。 ### 访问无障碍评分 访问无障碍评分的分值由相关指标的加权平均值计算而来。可以在[评分详情](https://docs.google.com/spreadsheets/d/1Cxzhy5ecqJCucdf1M0iOzM8mIxNc7mmx107o5nj38Eo/edit#gid=0)查阅每项指标的具体权重。同理,较大权重的指标项对分数的影响较大。无障碍性的每个指标项测试结果为`pass`或者`fail`,与性能指标项的计算方式不同,当页面只是部分通过某项指标时,页面的这项指标将不会得分。例如,如果页面中的一些元素有`屏幕阅读器友好`的命名,而其他的元素没有,那么这个页面的 screenreader-friendly-names 指标项得分为0。 ### 最佳实践评分 最佳实践评分的分数区间为0-100。影响这项评分的指标项的权重都是相同的。如果我们想计算某个指标项对总体评分的贡献,用指标项的数量除以100即可。指标项和权重同样可以参考上文提供的评分详情表,我们可以结合测评结果一一进行优化。 ================================================ FILE: appendix01/3-lighthouse-case.md ================================================ ## Lighthouse 使用案例 前面两节我们对 Lighthouse 的使用方法和性能评分计算的原理有了一定的了解,下面我们使用 Lighthouse 对一个实际站点 [https://lavas.baidu.com/](https://lavas.baidu.com/) 进行测试,看看 Lighthouse 能对站点提供哪些方面的改进指导。 ### 对站点进行测试 为了更直观地展示测试结果,我们使用 Chrome Devtools 中集成的 Lighthouse 工具进行测试。 1. 在 Chrome 中打开 https://lavas.baidu.com/ 2. 打开 Chrome Devtools,并选择 `Audits` 面板 3. Lighthouse 提供了不同的模拟网络环境选项,我们可以根据需求进行选择,点击 `Run Audits` 开始测试。 ### 测试结果 稍等片刻,Chrome 会新建一个新的会话窗口,将包含各项评分指标的 Lighthouse 测试报告展示出来,我们可以依次来看: #### 总体评分 报告的最上方是总体评分,如下图,可以看到站点在性能、PWA、最佳实践及 SEO 方面的表现还是不错的,在无障碍访问指标项上,评分稍低,需要重点进行改进。 ![overall](./img/lavas_overall.png) 具体到每一个方面,都能查看指标项的细节: #### Performance 性能评分 在选择网络环境为 `No throttling` 的条件下,FCP,FMP,首次交互时间等指标都能得到详细的统计,`View Trace` 还能看到页面每一帧的渲染过程。 ![performance](./img/performance.png) Metrics 部分的指标与性能评分关联紧密,可以看到各个指标项都显示绿色,意味着它们都得到了 90-100 的分值。Opportunities 和 Diagnostics 部分的指标项虽然不纳入评分的计算,但也给我们改进提供了参考。可以看到站点存在字体文件加载优化、静态资源缓存等问题。 ![Diagnostics](./img/diagno.png) #### PWA 评分 在基准指标检查中,有一项未通过:Lighthouse 建议站点需要对 JavaScript 脚本不能正确加载的情况作降级处理。我们需要改进的是,页面的核心渲染脚本加载失败时,仍能够展示基本的静态内容,而不是空白页面,这一点在客户端渲染的 SPA 应用中尤其需要引起重视。除此之外,我们可以看到站点的其他 PWA 自动检查项都已经通过,然后手动检查一下跨浏览器兼容性、页面切换流畅度、页面 URL 等检查项即可。 ![PWA](./img/pwa.png) #### 无障碍访问评分 ![accessibility](./img/access.png) 此项评分偏低,仅得到62分,说明站点在这部分存在的问题较多。展开可以看到问题的详情,如 DOM 元素使用不规范、结构不合理,页面颜色对比度不够,Meta 信息使用不正确等等,如果需要提高站点的无障碍可访问性,我们可以从这些方面对症下药进行完善。 #### Best Practices 最佳实践评分 站点在这部分的表现中规中矩,通过了15个检查项中的12个,暴露了3个问题,资源推荐使用 http2,跨域的跳转链接需要使用 rel 标识,不能使用废弃的 API。不通过检查测试,开发中我们很难注意到这些细节问题。 ![practices](./img/best_practice.png) #### SEO 搜索引擎优化评分 ![seo](./img/seo.png) 结果显示站点的 robots.txt 文件缺失,需要改进。 ### 小结 通过上述简单演示,我们对站点的各项指标就能有较为直观的了解,评分高的指标项值得积累开发经验,评分低的指标,对照着测试报告给出的建议,我们可以逐一完善。 Lighthouse 是一个开源的项目,主要由 Google Chrome 团队开发维护,它包含了强大的功能和较为繁荣的周边生态,本章只是就其使用方式、打分机制、测评指标等作了简要的介绍,在实际的 PWA 工程中,我们可能会运用到更详细的配置选项和测试参数,或者需要深入了解其运行原理,这些都可以通过 Github 上的项目文档进一步学习和实践。工欲善其事,必先利其器,有了 Lighthouse 这把利器的帮助,我们打造体验更好的 PWA 站点将不再是难事! ================================================ FILE: appendix01.md ================================================ # 使用 Lighthouse 测评 PWA 完成了 PWA 站点的开发之后,我们需要对站点进行评估和测试,了解其性能是否达标、是否符合 PWA 规范等。本章中,我们将介绍一款 Web App 的测评工具 —— Lighthouse 的使用,通过它的帮助,我们能够进一步了解 PWA 的缺陷和不足,有针对性地对站点进行完善和改进。 ================================================ FILE: book.json ================================================ { "title": "PWA 应用实战", "description": "PWA 开发实战", "author": "lavas ", "language": "zh-hans", "gitbook": "3.2.3", "root": ".", "structure": { "readme": "README.md" }, "plugins": [ "-lunr", "-search", "anchors", "search-plus", "-livereload", "simple-page-toc", "prism", "prism-themes", "-highlight", "advanced-emoji", "include-codeblock", "-sharing", "sharing-plus", "ace", "emphasize", "splitter", "page-footer-ex", "expandable-chapters-small", "sectionx", "local-video", "anchor-navigation-ex", "favicon", "todo", "edit-link", "3-ba", "chart", "disqus", "github" ], "pluginsConfig": { "edit-link": { "base": "https://github.com/lavas-project/pwa-book/edit/master", "label": "编辑此页" }, "theme-default": { "showLevel": false }, "prism": { "css": [ "prismjs/themes/prism-okaidia.css" ] }, "include-codeblock": { "template": "ace", "unindent": true, "edit": true }, "sharing": { "douban": true, "facebook": false, "google": false, "hatenaBookmark": false, "instapaper": false, "line": false, "linkedin": true, "messenger": false, "pocket": false, "qq": false, "qzone": false, "stumbleupon": false, "twitter": false, "viber": false, "vk": false, "weibo": true, "whatsapp": false, "all": [ "facebook", "google", "twitter", "weibo", "instapaper", "linkedin", "pocket", "stumbleupon", "qq", "qzone" ] }, "page-footer-ex": { "copyright": "Copyright ©2019 [百度 Web 生态团队](https://github.com/lavas-project)
[![知识共享许可协议](https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png)](http://creativecommons.org/licenses/by-nc-nd/4.0/) 本书采用 [知识共享署名-非商业性使用 4.0 国际许可协议](http://creativecommons.org/licenses/by-nc-nd/4.0/) 进行许可。
本书仍在勘误阶段,请勿以任何形式私自传播,我们保留对本书的版权", "markdown": true, "update_label": "该文件修订时间:", "update_format": "YYYY-MM-DD HH:mm:ss" }, "anchor-navigation-ex": { "showLevel": false, "isRewritePageTitle": false, "tocLevel1Icon": "fa fa-hand-o-right", "tocLevel2Icon": "fa fa-hand-o-right", "tocLevel3Icon": "fa fa-hand-o-right" }, "sectionx": { "tag": "b" }, "favicon": { "shortcut": "favicon.ico", "bookmark": "favicon.ico" }, "disqus": { "shortName": "lavas-project-github-io" }, "terminal": { "copyButtons": true, "fade": false, "style": "flat" }, "3-ba": { "token": "c45e843439f6364ead89fd505f1abb68" }, "github": { "url": "https://github.com/lavas-project/pwa-book" } } } ================================================ FILE: chapter01/1-how-was-pwa-born.md ================================================ # 为什么会出现 PWA PWA 是 Progressive Web Apps 的缩写,翻译为渐进式网络应用。早在 2014 年, W3C 就公布过 Service Worker 的相关草案,但是其在生产环境被 Chrome 支持是在 2015 年。因此,如果我们把 PWA 的关键技术之一 Service Worker 的出现作为 PWA 的诞生时间,那就应该是 2015 年。在关注 PWA 是什么之前,先来了解一下为什么会出现 PWA,它是在什么样的背景下被提出来的。 回顾一下历史,在 2015 年之前的那段时间,作为前端开发人员,我们主要精力花在哪里,对于我来说,移动站点的性能优化是投入精力很大的一部分,例如提升首屏速度,动画的流畅度,经过一段时间的优化,性能确实有不小的提升,但是无论怎么优化,还是比 Native App 要差很多,始终无法突破移动设备上 WebView 给 Web 的枷锁,这就是我们想说的第一个问题,**Web 的用户体验**。 除开用户体验问题之外,还有一个非常重要的问题,那就是**用户留存**。Native App 安装完毕后会在用户手机桌面上有一个入口,让用户打开 App 只需一次点击,而 Web App 在移动时代最主要的入口还是搜索引擎,用户从浏览器到站点需要经过搜索引擎,如果想访问上次同样的内容甚至还需要记住上次的搜索词,用户也可以记住 URL 并进行输入,但这些对于移动用户来说,无疑成本巨大,这就导致 Web 站点和用户之间的粘性非常脆弱。Native App 还能够通过发送通知让用户再次回到应用中来,而 Web 没有这个能力。 最后要说的一个问题是 0 和 1 的问题,**Device API 的不完善**。Android 和 iOS 提供了非常丰富的设备 API,Native App 只需获取用户授权就可以使用,而在 Web App 中,WebView 没有提供这样的 API,完全没法使用,如果我们开发一个需要使用 NFC 的 App,你一定不会考虑 Web,因为近场通信 API 在 Web 中还没有。虽然在近年来,W3C 已经提出了很多新的标准,但是浏览器对于 Device API 的支持仍然很不完善。 Google 在一篇名为《[Why Build Progressive Web Apps](https://developers.google.com/web/ilt/pwa/why-build-pwa)》的文章中披露过这样的一组数据,Web 站点每个月的 UV 是 Native App 的 3 倍,然而用户在 Native App 花费的时间却是 Web 的 20 倍,如下图所示,这之间巨大的反差,和上面所说的三个原因息息相关。 ![Web 站点与 Native App 的 UV 和用户时长对比](./img/web_vs_na_uv_user_duration.png) 就在这样的背景条件下,PWA 以及支撑 PWA 的一系列关键技术应运而生。 ================================================ FILE: chapter01/2-what-is-pwa.md ================================================ # 什么是 PWA Google 提出 PWA 的时候,并没有给它一个准确的定义,经过我们的实践和总结, PWA 它不是特指某一项技术,而是应用多项技术来改善用户体验的 Web App,其核心技术包括 Web App Manifest,Service Worker,Web Push 等,用户体验才是 PWA 的核心。 PWA 主要特点如下: * 可靠 - 即使在网络不稳定甚至断网的环境下,也能瞬间加载并展现 * 用户体验 - 快速响应,具有平滑的过渡动画及用户操作的反馈 * 用户黏性 - 和 Native App 一样,可以被添加到桌面,能接受离线通知,具有沉浸式的用户体验 PWA 本身强调**渐进式**(Progressive),可以从两个角度来理解渐进式,首先,PWA 还在不断进化,Service Worker,Web App Manifest,Device API 等标准每年都会有不小的进步;其次,标准的设计向下兼容,并且侵入性小,开发者使用新特性代价很小,只需要在原有站点上新增,让站点的用户体验渐进式的增强。 Google 在官网一篇名为《[Progressive Web App Checklist](https://developers.google.cn/web/progressive-web-apps/checklist)》的文章中给出了 PWA 的基准线,也给出了体验更好的示范性 PWA 的 Checklist,下面列出了 PWA 的最低要求。 * 站点需要使用 HTTPS * 页面需要响应式,能够在平板和移动设备上都具有良好的浏览体验 * 所有的 URL 在断网的情况下有内容展现,不会展现浏览器默认页面 * 需要支持 Wep App Manifest,能被添加到桌面 * 即使在 3G 网络下,页面加载要快,可交互时间要短 * 在主流浏览器下都能正常展现 * 动画要流畅,有用户操作反馈 * 每个页面都有独立的 URL ## PWA 的特性 PWA 本质上还是 Web App,借助了新技术具备了一些 Native App 的特性,所以它兼具 Web App 和 Native App 的优点,同时在安全、体验和用户黏性三个方面都有很大的提升。总结下来,PWA 具有如下特性。 * **渐进式** - 适用于所有浏览器,因为它是以渐进式增强作为宗旨开发的 * **连接无关性** - 能够借助 Service Worker 在离线或者网络较差的情况下正常访问 * **类原生应用** - 由于是在 App Shell 模型基础上开发,因此应具有 Native App 的交互,给用户 Native App 的体验 * **持续更新** - 始终是最新的,无版本和更新问题 * **安全** - 通过 HTTPS 协议提供服务,防止窥探,确保内容不被篡改 * **可索引** - manifest 文件和 Service Worker 可以让搜索引擎索引到,从而将其识别为『应用』 * **黏性** - 通过推送离线通知等,可以让用户回流 * **可安装** - 用户可以添加常用的 Web App 到桌面,免去到应用商店下载的麻烦 * **可链接** - 通过链接即可分享内容,无需下载安装 PWA 的这些新特性给 Web App 注入了活力,而 Native App 却没能很好的弥补自己的劣势。对于 Native App来说,最大的痛点是由于其天生封闭的基因,内容无法被索引,这会导致 Native App 很难被分发,例如,用户想知道红烧肉的做法,还需要先知道应用的名称,下载应用之后才能获取内容,这个流程十分不合理,根据 Google 的统计,用户每个月安装的应用个数约等于 0,再加上用户 80% 的时间被 Top3 的超级应用占据,应用分发成本也因此越来越高。相对于 Native App 的封闭,PWA 完全是开放的,PWA 的所有技术都是遵循开放的标准,因此能够被浏览器快速支持,被开发者接受。 下表列出了传统 Web App,Native App 和 PWA 在各特性的对比。 | |是否可安装|是否可链接访问|用户体验|用户黏性| |---|---|---|---|---| |传统 Web|无法安装|可链接访问|体验一般|黏性差| |Native App|可安装|不可链接访问|体验好|黏性强| |PWA|可安装|可链接访问|体验好|黏性强| PWA 能给站点体验带来飞跃式的提升,我们可以用移动设备上的浏览器,如 Chrome, 访问 [LAVAS 官网](https://lavas.baidu.com) 体验一下,并添加到桌面,还可以在断网的情况下使用。现在在国内也有很多 PWA 站点,比如饿了么和新浪微博的移动版,不用耗费流量下载几十兆的应用,就能有和原生应用一样的体验,不妨尝试一下。 在后面的章节中,我们会从体验、安全和性能三个角度来分析如何打造一个好的 PWA。 ================================================ FILE: chapter01/3-what-are-key-techs.md ================================================ # PWA 的核心技术 前文提到,PWA 的核心是用户体验,能让 PWA 达到原生应用的体验并不仅仅依赖于某一项技术,而是多管齐下,进行改进,从而在安全、性能和体验上都获得很大的提升。下面将简单介绍几个 PWA 应用中经常使用到的技术,后面的章节会进行更详细的讲解。 ## Web App Manifest Web App 是依附于浏览器的,在一般情况下,用户需要先打开浏览器,然后输入网址或点击收藏的书签,才能够访问到网页内容,相比在移动设备主屏上拥有一席之地的 Native App,Web App 使用起来太麻烦了,这也是 Native App 相比 Web App 用户黏性更好的原因之一。因此 Web App 也需要这个能力,Web 应用清单(Web App Manifest)能够帮助我们实现这一点,也是 PWA 最核心的功能之一,开发者可以定义用户添加到主屏的图标、应用名称等,也许有人会说,有些移动设备上的浏览器可以在主屏上添加网页的快捷方式,但其实用户体验区别很大,Web App Manifest 允许开发者配置隐藏浏览器多余的 UI(地址栏,导航栏等),让 PWA 具有和 Native App 一样的沉浸式体验。 Web App Manifest 体现在代码上主要是一个 JSON 文件:`manifest.json`,开发者可以在这个 JSON 文件中配置 PWA 的相关信息,应用名称、图标、启动方式、背景颜色、主题颜色等等。添加到桌面后,PWA 并不是一个快捷方式,而是能够在系统中作为一个独立的 App 存在的,用户可以设置它的权限,清除它的缓存,就和 Native App 一样。 添加主屏的好处是显而易见的,首先它缩短了用户和站点的距离,用户可以在主屏直达站点;其次是能够让网站具有更加接近 Native App 的体验,具有启动画面、沉浸式浏览体验;最后,PWA 会被系统的应用商店收录,目前只有 Windows 10 这样做了,但是可以预见在不远的将来,其他的主流平台也会进行收录。 ## Service Worker 在前文中,频繁的提及 Service Worker 是因为它真的很重要,毫不夸张的说,Service Worker 就像人体中心脏一样的存在,如果没有它,PWA 就像没有了动力,无法寸进。 Service Worker,直白的翻译就是服务工作线程,但一般我们不会这么做。它是浏览器在后端独立于网页主进程运行的脚本,它可以拦截网络请求,可以操作本地缓存,还可以接受服务器推送的离线消息,它的功能很丰富,并且 Service Worker 可扩展性很强,想象空间比较大,未来 PWA 很多的特性会基于 Service Worker 来设计,这也是笔者为什么说它是 PWA 的心脏。 简单归纳一下,Service Worker 的特点,如下: * 一个特殊的 worker 线程,独立于当前网页主线程,有自己的执行上下文 * 一旦被安装,就永远存在,除非显式取消注册 * 使用到的时候浏览器会自动唤醒,不用的时候自动休眠 * 可拦截并代理请求和处理返回,可以操作本地缓存,如 CacheStorage,IndexedDB 等 * 离线内容开发者可控 * 能接受服务器推送的离线消息 * 异步实现,内部接口异步化基本是通过 Promise 实现 * 不能直接操作 DOM * 必须在 HTTPS 环境下才能工作 ![服务器浏览器桥梁图](./img/server_browser_service_worker.png) Service Worker 在 PWA 中最重要的功能就是离线与缓存,在本书第三章,还会有很多笔墨来介绍 Service Worker 如何实现站点离线。 ## 离线通知 Service Worker 是 PWA 很多功能的基础,正是有了 Service Worker,其他功能才能发挥更大的作用,离线通知就是其中之一。 离线通知是指在用户没有打开 PWA 站点的情况下,也能接受到服务器推送过来的通知并展现给用户,其中包括了两部分,离线推送和展现通知,分别是 Web Push 和 Notification API。 推送通知是一种时效性非常强的与用户沟通的方式,即使在 PWA 没有打开的情况下,依然可以触达用户,能够立即引起用户的注意,对于一些突发事件、限时活动、重大升级等时效性要求很高的场景,推送通知总是最好的选择,这也是过去 Native App 强于 Web App 的原因之一。因此 PWA 提供了 Web Push 和 Notification API 补全了这一功能。 浏览器在接受到对应的消息服务中心推送过来的离线消息时,会唤醒对应站点注册的 Service Worker,开发者可以在 Service Worker 文件中处理接受到的请求,显示通知。 Web Push 和 Notification API 在后面的章节也会重点介绍。 ## App Shell 和骨架屏 App Shell 是 PWA 强调的一个非常重要的设计理念,它能够缩短用户进入页面时的白屏时间,让用户一进入 PWA 就能快速看到 PWA 的整体框架,就和 Native App 一样。从概念上讲,App Shell 是 PWA 界面展示所需的最小资源集合,即让页面能够正常运行起来的最小的 HTML、CSS 和 JavaScript 等静态资源集,每个页面都需要加载这一部分资源。利用 Service Worker 把这部分资源缓存在本地,就能够在打开 PWA 时不需要从服务器端获取这部分资源,从而能够瞬间渲染出页面框架,不仅提升了首屏的速度,还减小了站点流量的消耗。 骨架屏(App Skeleton),也是提升首屏体验的有效方式。它的原理是在真实内容渲染完成之前,使用一些能够快速渲染的静态图片/样式/色块/部分真实内容进行占位,让用户对真实内容区域有心理预期。App Shell 和骨架屏都是提升首屏体验的绝好妙招。 App Shell 和骨架屏相辅相成,App Shell 显示页面的外框部分,初始内容就用骨架屏来填充,保证主体内容区域不会留白,它的特点是: * 在页面加载初期预先渲染内容,提升感官上的体验 * 一般情况骨架屏和实际内容的结构是类似的,因此之后的切换不会过于突兀。这点和传统的 Loading 动图不同,可以认为是其升级版 * 只需要简单的 CSS 支持 (涉及图片懒加载可能还需要 JS ),不要求 HTTPS 协议,没有额外的学习和维护成本 * 如果页面采用组件化开发,每个组件可以根据自身状态定义自身的骨架屏及其切换时机,同时维持了组件之间的独立性 App Shell 和骨架屏在提升首屏体验上发挥了重要作用,本书第二章会首先介绍,在了解了这部分内容之后,就能够很好的理解 App Shell 结合 Service Worker 的重要性。 以上四点是笔者认为对应 PWA 非常重要的技术,因此列出来,并且后续也会重点讲述,其他在这里没有提到的技术对于 PWA 来说也同样重要,在本书中不会重点讲述,但也会有所提及,开发者可以去阅读一些相关的文档或者标准,比如 [MDN](https://developer.mozilla.org/en-US/) 站点。 ================================================ FILE: chapter01/4-how-is-pwa-going.md ================================================ # PWA 的发展 从 PWA 被提出到现在,已经过去了 4 年,PWA 取得的成绩有目共睹,特别是在国外,在网络速度不够快或者相对贫困的地区,PWA 非常受欢迎,因为它不需要很高的硬件配置,也很省流量,比如在印度,就有一个很成功的案例,Flipkart。 ## Flipkart [Flipkart](https://www.flipkart.com) 是印度最大的电商公司,在 2015 年的时候,他们关停了 Web App,尝试将用户导流到 Native App,后来发现在产品快速迭代和好的用户体验之间很难做到两者兼得,因此他们决定将 Web App 和 Native App 整合为 PWA,通过 Service Worker,Web App Manifest 等技术的使用,PWA 不仅在体验上达到了他们的标准,同时还具有了 Web App 的产品迭代速度。 采用 PWA 后,取得的成绩斐然,关键收益如下: * 用户时长增加了 3 倍,传统 Web 是 70 秒,而 PWA 达到了 3.5 分钟。 * 用户回流率提升了 40%。 * 添加到主屏的用户转化率提高了 70%。 * 用户数据流量的消耗降低了 3 倍。 ![Flipkart Lite](./img/flipkart.jpeg) ### 阿里速卖通(AliExpress) [阿里速卖通](https://m.aliexpress.com)是阿里巴巴旗下的一款产品,对外销售来自中国的各种产品。AliExpress 的困境在于,用户不愿意下载安装他们的 Native App,即使在 Web App 中对 Native App 有足够的引流,导致获客成本很高。 最后,AliExpress 选择了 PWA,他们的出发点在于提升 Web App 的体验和用户黏性。带来的收益也非常的明显,如下: * 在 PWA 中,新用户的转化率提升了 104%。 * 在一次会话中,用户访问的页面数量增加了 2 倍。 * 用户时长增加了 74%。 ### 饿了么 不仅在国外,国内同样也有 PWA 的案例,[饿了么](https://ele.me),作为国内最早一批尝试 PWA 的站点,同样也取得了不小的收益。 饿了么做 PWA 站点的出发点和 Flipkart、AliExpress 不完全一样,它几乎纯粹是从性能优化的角度接入的 PWA,当时国内的浏览器市场比较混乱,对 PWA 支持的不是很完善。饿了么 PWA 采取的是多页应用(Multi-Page Application),和 PWA 推荐的 SPA(Single Page Application)不一样,主要是考虑到多个团队合作共同开发同一个站点,不过在体验上并没有打折扣,现在是 Google 对外展示的一个成功案例。 看一下饿了么 PWA 的收益: * 预缓存的页面加载时间缩短了 11.6%。 * 所有页面平均加载时间缩短了 6.35%。 * 在 3G 网络下,第一次加载首次可交互时间缩短了 4.93 秒。 ![饿了么 PWA](./img/eleme.png) ## 标准的支持 PWA 采用的最新技术,当前浏览器还没有达到完全支持的程度,很多技术在 W3C 还没有定稿,不过这也意味着这些技术的还有很大的想象空间。 根据 [Can I Use](https://caniuse.com) 的统计(包括 PC 和移动端,截至 2019 年 4 月 2 日),PWA 的关键技术在浏览器中的支持度如下: * Web App Manifest 的支持度达到 80.63%。 * Service Worker 的支持度达到 89.84%。 * Notifications API 的支持度达到 75.17%。 * Push API 的支持度达到 78.06%。 随着标准的进一步完善,国内外各大浏览器都会逐步支持,拥抱标准。Chrome 自不必说,Apple 从 iOS 11.3 版本开始在 Safari 上支持 Service Worker,iOS 12.2 版本修复了 PWA 很多致命的体验问题,支持了 Web Share API 等。可见大家都在拥抱标准,拥抱开放。 Can I Use 的统计由于一些原因在国内不是很适用,为此百度 Web 生态团队维护了一份列表,开发者可以在上面查看国内各主流浏览器对 PWA 主要技术的支持程度,[https://lavas.baidu.com/ready](https://lavas.baidu.com/ready)。 ================================================ FILE: chapter01/5-the-future-of-pwa.md ================================================ # PWA 的未来 从 Google 最初提出 PWA 到现在,PWA 已经有不小的改变了,这就是 Web 的魅力,遵循标准且完全开放的魅力,来自世界各地的开发者参与标准的制定,它还在不断进化,Web 即使已经 30 岁了,它还依旧是被广泛应用的技术之一。 关注 Web 标准化的开发者会在标准文档里发现很多有意思的提案,有 Web 蓝牙、Web XR 等,在 [TPAC Lyon 2018](https://www.w3.org/2018/10/TPAC/) 上,Intel 的开发者演示了他们开发的 Web Machine Learning 的 DEMO,Web 也能直接利用 NPU 来进行深度学习的计算,让我觉得 Web 还能再战 30 年,我对此充满信心。 在国外,PWA 已经被广泛应用,也被用户所接受。在笔者刚从事 Web 生态相关工作的时候,国内才刚刚接触到 PWA 这个概念,UC 浏览器的内核版本还是 Chrome 3x,连 ES6 都支持的不全,更别说是 Service Worker、Web App Manifest 等 PWA 技术了。UC 浏览器并不是个例,国内厂商 App 内核版本几乎都不支持 Service Worker。不过也就在不到一年的时间里,这些浏览器就都支持了 Service Worker,让人不得不感叹国内互联网进步之快,国内主流浏览器对 Service Worker 的支持度如下图所示。 ![Service Worker 的支持度](./img/is_service_worker_ready.png) 除了 Service Worker 等主流 PWA 技术外,W3C 也一直在推进 Device API 的标准。在 MDN,有一个 Web API 的索引,[WebAPI](https://developer.mozilla.org/zh-CN/docs/WebAPI),里面列出了大部分的现存的 Device API 和其他的 API。 [Accelerated Shape Detection in Imagges](https://wicg.github.io/shape-detection-api/) 是形体检测的 API,在最新的 Chrome 中已经支持,如下代码所示。 > 需要将 chrome://flags/#enable-experimental-web-platform-features 设置为 Enabled。 ```javascript // 人脸识别 const faceDetector = new FaceDetector({fastMode: true, maxDetectedFaces: 1}) // 假设 theImage 是 标签中的内容或者一个 Blob 对象 faceDetector.detect(theImage) .then(detectedFaces => { for (const face of detectedFaces) { console.log( ` Face @ (${face.boundingBox.x}, ${face.boundingBox.y}),` + ` size ${face.boundingBox.width}x${face.boundingBox.height}`); } }).catch(() => { console.error("Face Detection failed, boo."); }) ``` 不断有新的 Device API 被支持,W3C 等标准组织有一群对 Web 怀抱希望,希望 Web 成为开放技术的人,他们在努力推进 Web 用户体验的提升,虽然由于 W3C 的组织方式和对安全、隐私、性能的考虑,推进速度不是很快,但不久也会被所有浏览器支持。我喜欢 Web 的开放,喜欢它的简单。 ================================================ FILE: chapter01/6-your-first-pwa.md ================================================ # 你的第一个 PWA 本书中大部分示例均基于下面的这个模板展开,开发者可以跟随书中示例逐步操作,加深理解。在这个示例里,我们一起来实现一个能添加到桌面并且离线可用的 PWA。 ## 准备工作 在准备编写第一个 PWA 前,有一些准备工作需要准备,需要安装一些必备的软件,如下: * 一台可以正常联网的计算机并已安装较新版本的 Node.js,npm,Git * 一个方便调试并支持 Service Worker 的浏览器,推荐使用 Google Chrome * 一部安卓手机,开启添加到桌面的权限,推荐安装好 Chrome 浏览器 * 一个自己习惯的文本编辑器,如 Visual Studio Code, Sublime Text 等等 ## 下载代码 在完成上面的准备工作后,接下来下载笔者准备的示例代码。本书的示例代码均托管在 GitHub 上,地址是 [https://github.com/lavas-project/pwa-book-demo](https://github.com/lavas-project/pwa-book-demo)。 > 本书所有的 JS 代码均符合 [JavaScript Standard Style](https://standardjs.com/) 规范。 那么接下来的第一步,我们先下载代码到本地,在命令行中运行如下命令。 ```bash # 从 GitHub 下载代码到本地 pwa-book-demo 目录 $ git clone https://github.com/lavas-project/pwa-book-demo.git # 进入到 chapter01 目录 $ cd chapter01 # 安装 npm 依赖 $ npm install # 安装成功后启动 chapter01 示例 $ npm run server ``` 在看到命令行中输出 `Server start on: http://127.0.0.1:8088`,意味着已经成功启动,这时,打开浏览器,访问 `http://127.0.0.1:8088` 能看到如下图所示的页面。 PWA Chapter01 Demo 截图 接下来,开发者可以根据下面的步骤逐步开启 Web App Manifest 和 Service Worker,开始体验自己的第一个 PWA。 ## 添加到主屏 根据前面的章节介绍,增加用户黏性最好的方式是把这个 PWA 放在用户的主屏上,它背后的技术就是 Web App Manifest,接下来,我们就来看看如何使用。 第一步,站点需要新增一个文件:`manifest.json`,这个文件中包含站点的名称、图标地址、入口地址、显示模式等信息,并且通过一个地址能够访问到该文件,在我们下载下来的代码中,这个文件在 `chapter01/public/` 目录下,启动调试服务器后,可以通过 `http://127.0.0.1:8088/manifest.json` 访问到。 ```json { "name": "PWA Chapter01 Demo", "short_name": "Chapter01 Demo", "icons": [ { "src": "assets/images/icons/icon_144x144.png", "sizes": "144x144", "type": "image/png" }, { "src": "assets/images/icons/icon_152x152.png", "sizes": "152x152", "type": "image/png" }, { "src": "assets/images/icons/icon_192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "assets/images/icons/icon_512x512.png", "sizes": "256x256", "type": "image/png" } ], "start_url": "/index.html", "display": "standalone", "background_color": "#fff", "theme_color": "#1976d2" } ``` 第二步,在 HTML 页面中添加对 `manifest.json` 文件的引用,在示例中,打开 `chapter01/public/index.html` 文件,添加如下代码到 `` 中。 ```html ``` 第三步,在移动端浏览器 Chrome 中打开启动的地址,如果在同一个局域网内,可以通过电脑的 IP 地址访问,比如笔者的电脑的局域网 IP 是 `192.168.0.100`,那么就可以通过 `http://192.168.0.100:8088/` 访问。访问成功后,点击添加到桌面按钮,PWA 就会出现在主屏上,如下图所示。 ![在 Chrome 中添加到主屏](./img/add_to_homescreen.png) 点击 PWA 的图标,不仅具有启动画面,还具有完全沉浸式的体验,如下图所示。 ![PWA Chapter01 Demo 的打开效果](./img/pwa_open.png) Web App Manifest 的内容远不止这些,可以阅读本书的后续章节,会详细介绍。 > 如果添加到主屏始终不成功,可以阅读本书的第五章,相信会解决您的问题。 ## 离线可用 离线可用依赖于 Service Worker 的应用,接下来来看看在示例代码中如何启用 Service Worker。 第一步,您需要一个 Service Worker 文件,在示例中,可以查看 `chapter01/public/sw.js` 文件,并且可以通过 `http://127.0.0.1:8088/sw.js` 访问到该文件,sw.js 文件具体的内容会在后续章节逐步讲解。 第二步,在 HTML 页面中注册 Service Worker。打开 `chapter01/public/index.html` 文件,找到下面的代码,并将注释打开。 ```html ``` 第三步,接下来可以通过打开电脑上的 Chrome 来验证是否注册成功,并且是否离线可用。 通过 Chrome 访问 `http://127.0.0.1:8088` 打开我们的第一个 PWA,并且打开调试工具,点击 `Application` 菜单栏,再点击调试工具左边的 Service Worker 选项,会看到 `sw.js` 的注册信息,如下图所示: ![成功注册 Service Worker](./img/sw_register_success.png) 那么,我们检查一下是否真的离线可用,勾选上图中的 `Offline` 复选框,让 Chrome 在这个标签页下保持断网状态。 ![勾选 Offline 复选框](./img/offline.png) 接下来,刷新页面,您会发现页面依然能正常渲染,这就是 PWA 的离线可用,可以利用 Service Worker 做很多事情,缓存页面框架和骨架屏,提升页面首屏速度,甚至可以缓存部分数据。Service Worker 将在本书的后续章节会重点讲述。 ## 总结 以上就是您的第一个 PWA,麻雀虽小,五脏俱全,能被添加到主屏,能离线可用,短短的几行代码就在原站点的基础上实现了这两个功能,并且没有侵入性,代价也很小,它确实奉行渐进式的原则。 ================================================ FILE: chapter01.md ================================================ # 什么是 PWA 随着移动互联网的发展,Native App 开始兴起,那么 Web App 落伍淘汰了吗?很显然答案是没有,它依然生机勃勃,和 Native App 互相依存,还衍生出很多 Native App 和 Web App 相结合的技术,比如 Hybrid App,React Native 等,Angular/ReactJS/Vue 也都是在这期间才发展起来的优秀的框架,可见,Web App 仍在不断进化,PWA(Progressive Web Apps)就是在这背景下诞生的。 本书第一章会着重介绍 PWA 的基础概念,让读者对 PWA 有个大致的了解,后续章节会再深入讲解。本章会探讨为什么会出现 PWA,PWA 的出现解决了哪些问题,然后再看 PWA 的主要技术,并且会结合实际的例子让读者能够创建一个简单的 PWA。 ================================================ FILE: chapter02/1-what-is-good-ux.md ================================================ # 什么是好的用户体验 PWA 的核心是用户体验,它的核心技术(如 Service Worker,Web App Manifest 等)都是为了提升 Web App 用户体验,但“体验”其实是个很主观的感受,我们很难用一个或几个量化指标来轻易的衡量用户体验,判断优劣,甚至不同的人有不同的理解,不过体验好的站点都有一些共性,包括不限于下面列出来的一些特征。 * 首屏速度快 * 顺滑流畅的动画效果 * 有用户操作的反馈 * 比较简单的操作步骤 * 主体内容比较在最显眼的位置 * 整站体验一致 * 无障碍访问,不同的人群均可使用 用户体验的核心是用户,设计师需要站在用户的角度思考用户需要什么,在做设计的时候需要做充分的调研。移动设备上的用户目的性很强,需要在巴掌大小的屏幕上快速找到自己想要的内容。 很多公司都沉淀了自己的一套设计原则和方案,百度在 2018 年发布了《[百度移动搜索落地页体验白皮书 4.0](https://ziyuan.baidu.com/college/documentinfo?id=2492)》,这里面列出了很多体验上的指导原则。Google 也在开发者网站上列出了他们总结的 25 项设计原则,《[What Makes a Good Mobile Site?](https://developers.google.cn/web/fundamentals/design-and-ux/principles/)》。 笔者将他们整合了一下,并且加入了一些自己认为重要的设计原则,如下: 1. 主操作区域要放在显眼位置 2. 不要出现巨大的弹层盖住主要内容,比如下载条幅可以放在顶部并且添加关闭按钮 3. 推送的通知需要满足准确、准时和相关三个特征 4. 尽量减少用户的输入或者减小输入的代价,自动帮用户填写已知的数据,选择合适的 `input` 类型 5. 缩短不必要的流程,让整个转化流程更简洁 6. 响应式布局,让站点在不同尺寸的浏览器上都有好的体验 7. 图片清晰,且点击可放大查看更清晰的图片 8. 避免不必要的切换,让用户在一个浏览窗口内完成所有的操作 9. 如果需要申请设备权限,如定位、通知等,在申请前需要明确的告诉用户为什么需要这些设备权限 10. 可点击区域的宽度和高度不应小于 48px 11. 整站体验需要一致,页面框架、主色调等需要保持统一 好的设计原则远不止上面列出来的这些,好的设计不仅需要美观,还需要方便易用。 那么站在开发者的角度,哪些需要重点关注呢? ================================================ FILE: chapter02/2-design-and-tech.md ================================================ # 设计与技术 好的设计没有好的技术来实现同样无法达到最佳的用户体验。 设计与技术并不只是实现的关系,而是需要互相配合,再完美的设计如果不能实现,那也只是白费,而一些技术上的优化也需要设计给予配合才能更加完美。 上一节中,列出了很多设计原则,有些是比较容易实现的,但有些也比较复杂,复杂到需要很大的篇幅来描述。下面我们总结了几个比较重要的设计与技术相结合的问题。 ## 首屏速度 首先是首屏速度,它对于站点的重要性不言而喻,特别是移动端,下图是页面加载时间与跳出率和会话的关系。 ![页面加载时间与跳出率还有会话数量息息相关](./img/page_load_time_bounce_rate.png) 那么技术上如何提升首屏速度呢?首选我们会想到将 CSS 放在头部,JS 放在页面底部等技术手段,这些方法确实卓有成效,但是有没有更好的方法呢? 在这里就要讲到设计与技术相辅相成的一种 Web 的设计,那就是 App Shell,以及和 App Shell 配套使用的骨架屏(App Skeleton),本章后续会详细介绍。 ## 响应式布局 移动设备种类越来越多,屏幕大小不一,有小到高宽不足 40 毫米的智能手表,也有高度接近 300 毫米的 iPad。尽管如此,大部分的 Web 站点都没有对尺寸不一的设备做过适配,导致在移动端体验很差,这时就凸显响应式布局的重要性。 响应式布局不是一个纯技术的问题,在设计之初就需要设计师考虑众多问题,例如在不同尺寸的设备上,字体大小设置多少,页面如何布局,主体内容放在哪里,叠起来的内容如何使用户更方便的找到,小尺寸屏幕如何展现列数较多的表格,等等。 而开发者需要考虑如何实现根据屏幕尺寸大小动态调整字体大小或者布局,其中涉及到的知识点很多,在本章后续小节中会详细介绍。 ## 流畅的动画 动画是体验非常重要的一部分,站点展现速度再快,没有任何交互动画,也会显得没有生气,它赋予了站点“生命”。但是如果动画掉帧,造成卡顿,反而会让用户觉得站点体验很差,所以,我们需要的是流畅的动画。 动画的设计没有想象中的简单,过多的动画和没有动画体验同样糟糕,动画过多会让用户觉得烦躁,因此需要设计师在初期考虑到参与动画的元素、动画的类型、动画的持续时间等,而开发者需要考虑如何实现流畅的动画。在本章后续小节中同样会展开介绍。 ================================================ FILE: chapter02/3-app-shell.md ================================================ # App Shell 在第一章中已经简单介绍过 App Shell,这个小节我们会更深入学习如何构建和使用 App Shell。它并不是一种新的技术或者 API,而是设计与技术相结合产生的一种整站设计方案,减少用户进入页面的等待时间,用户能够快速看到页面的主体结构,虽然这时主体内容没有渲染出来,但是给用户感官上的体验是这个页面渲染很快,那么来看一下什么是 App Shell。 ## 什么是 App Shell PWA 多数采用单页应用(Single Page Application)的方式编写,这样能减少页面跳转带来的开销,并且开发者可以在页面切换时增加过渡动画,避免出现加载时的白屏。那么在页面切换时页面上固定不动的内容就是 App Shell 的一部分。 应用从显示内容上可以粗略的划分为内容部分和外壳部分。App Shell 就是外壳部分,即页面的基本结构,如下图所示: ![App Shell 与主体内容示意图](./img/appshell.png) 上面只是 App Shell 显示的部分,那么是谁去加载内容区域呢,是谁接管了页面的状态呢,是谁初始化页面的样式呢? 也是 App Shell,它不仅包括用户能看到的页面框架部分,还包括用户看不到的代码逻辑。因此,我们可以总结一下 App Shell 的定义,App Shell 是页面能够展现所需的最小资源集合,即支持用户界面所需的最小的 HTML、CSS 和 JavaScript 等静态资源集合。采用 App Shell 的站点,每个页面都会先加载 App Shell 的内容,再由 App Shell 根据当前页面 URL 渲染对应的主体内容。 上图给出了基本的 App Shell 例子,它包含头部导航,侧边栏等,对于一个应用来说它有一个相对稳定的结构。但是一个站点可以有多个 App Shell,以电商网站举例,不同品类的商品主题馆,颜色不同,甚至结构也会有一些区别,这就需要开发者灵活处理,可以分为多个 Shell,或者在一些页面去掉 Shell。 ## 如何正确使用 App Shell 单独使用 App Shell 并不是一个很好的主意,对于速度要求比较高的站点来说,它会导致用户看到主要内容的时间延后,那么有没有办法呢?这个时候开发者会提到使用浏览器的 HTTP 缓存,这确实是一个不错的主意,但它也有一些很难解决的问题。 * 难以确定合适的 HTTP 缓存设置时间,也不适合更新比较频繁的站点 * HTTP 缓存只有在 URL 相等的情况下才能起作用,这不符合 PWA 每个页面有唯一 URL 的要求 那么有没有更好的缓存方案呢?在第一章中,我们提到了 Service Worker 可以拦截网络请求和操作本地缓存,这给了我们足够的想象空间,可以结合 App Shell 和 Service Worker 来解决缓存问题。 1. 使用 Service Worker 预缓存 App Shell 的静态资源 2. 用户访问 Web 站点时,通过 Service Worker 拦截请求 3. Service Worker 返回缓存中的 App Shell 给浏览器 4. App Shell 根据当前的 URL 再去请求对应的数据来渲染 这样可以解决上面提到的所有问题。下面是 App Shell 和 Service Worker 结合使用的收益。 * 第二次访问速度极快且稳定。由于 App Shell 的内容已经缓存在本地缓存中,用户第二次访问会在极短的时间内渲染出 App Shell * 为用户节省流量。用户在后续的访问都不会再请求 App Shell 的内容,而是只请求主体内容,不用加载一些公用的静态文件。 * 具有 Native App 的用户体验。无论是第一次访问还是后续页面的切换,都具有唯一不变的区域,没有传统 Web 页面切换的白屏。 ## App Shell 的案例 [https://lavas.baidu.com](https://lavas.baidu.com) 是百度 Web 生态团队开发的 Lavas 的官网。这是一个体验比较好的 PWA 站点,那么来分析一下这个站点是如何结合 Service Worker 和 App Shell 的。 下图右侧就是 Service Worker 缓存在 [Cache Storage](https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage) 中的 App Shell 内容,左侧顶上的 banner 就是 App Shell 的可见区域。整个页面的外壳部分被缓存起来,在下次访问时由 Service Worker 取出并直接返回,如果把网络断开再刷新页面,页面依然能够正常渲染。 ![Lavas 官网 Service Worker 缓存的 App Shell 内容](./img/lavas_official_site.png) 经过我们前面的分析,App Shell 和 Service Worker 结合首屏展现速度会非常快,那么在这个案例中,首屏的性能到底如何呢,来看一下下面这张图,这张渲染性能的图是在 PC Chrome 上统计的。 ![Lavas 官网的首屏渲染速度](./img/lavas_official_site_performance.png) 从上图中,能看到大概在时间轴 200ms 的时候,已经渲染出了 App Shell。查看浏览器统计的详细 Event Log,上图中页面首次绘制是在 230ms,再减去上一个页面的销毁时间大概 80ms,实际上当前页面从开始发送请求到首屏渲染出来所花掉的时间是 **150ms**,对于用户来说,页面几乎是瞬间呈现,没有白屏时间。并且从上图中,也能看出渲染的顺序是先 App Shell,然后再渲染出主体内容,和我们预想的一样。 如果您觉得这个站点过于简单,无法体验 Service Worker 和 App Shell 结合的优势,下面列出了该团队提供的另一个较复杂的使用了 App Shell 模型的例子,也同样是体验非常良好的 PWA 站点。 [https://lavas.baidu.com/12306/](https://lavas.baidu.com/12306/),这是百度 Web 生态团队开发的一个 demo,参考中国铁路在线售票系统 12306。 > 注意,这个站点只是 demo,不是 12306 官方的网站,不要输入密码等个人信息。 ## 如何在 Vue 项目中使用 App Shell + Service Worker 第一章的例子也采用了 App Shell + Service Worker 的方式,但是没有采用任何框架辅助,在实际项目中,这样不借助任何框架编写 SPA 是比较少见的。在现代项目中,采用 Angular/React/Vue 来编写复杂 SPA 的项目占大多数,那么如何在这些框架中使用 App Shell 和 Service Worker 呢,下面我们用一个实际的例子来演示一下。 ### 启动示例 和第一章的例子一样,您需要准备 Git,Node.js 和 npm 环境,准备完成后,可以通过下面的命令下载我们的示例,这个示例是根据 `vue-cli` 创建的很基本的 Webpack 模板,可以参考 npm 上的文档 [vue-cli 简介](https://www.npmjs.com/package/vue-cli)。 ```bash # 从 GitHub 下载代码到本地 pwa-book-demo 目录 $ git clone https://github.com/lavas-project/pwa-book-demo.git # 进入到 chapter02/appshell 目录 $ cd chapter02/appshell # 安装 npm 依赖 $ npm install # 启动 chapter02 appshell 示例 $ npm run dev ``` 在命令行中看到输出 `Your application is running here: http://localhost:8080` 后,代表服务已经启动成功,打开浏览器,访问 `http://localhost:8080`,能看到下图所示的页面。 App Shell demo 启动成功示意图 这时,如果您打开 Chrome 调试工具,并定位到 Application 栏目,会发现没有注册 Service Worker,这是因为我们在开发模式下没有生成 `service-worker.js` 文件,根据我们的经验,在开发模式下启用 Service Worker 对开发调试会带来很大的影响,因为如果 Service Worker 写的不够完善,会导致开发中修改后的文件得不到及时的更新,没有经验的开发者会花较多的时间来排查,因此我们的 demo 只有在编译之后才会生成 `service-worker.js` 文件。 ```bash # 编译 $ npm run build # 进入到编译后的 dist 目录 $ cd dist # 安装静态文件调试服务 $ npm install -g edp # 在 dist 目录中启动本地静态文件服务 $ edp ws start ``` 在命令行输出了下面的文本后,即代表服务已经启动。 ``` mockservice edp INFO EDP WebServer start, http://xxx.xxx.xxx.xxx:8848 ``` 打开浏览器,输入 `http://localhost:8848`,能看到调试模式下相同的页面。不同的地方在于,这个时候已经安装了 Service Worker,打开 Chrome 调试工具,定位到 Application -> Service Worker,如下图所示。 ![Chapter02 demo Service Worker 安装成功示意图](./img/chapter02_demo_service_worker.png) 点击 Chrome 调试工具左侧的 Cache -> Cache Storage,能看到有两个 Cache Storage 实例,其中一个以 `sw-precache-` 开头的实例里面预缓存了 App Shell 所需的静态文件,如下图。 ![Chapter02 demo App Shell 缓存内容](./img/chapter02_demo_app_shell_in_cache_storage.png) ### 示例解析 #### 哪些区域是 App Shell 在这个 Vue 的示例里,可见部分哪些是 App Shell 呢,我们可以查看 `src/App.vue` 文件。 ```html ``` 熟悉 Vue 的开发者都知道 `` 标签在 Vue 项目里是用来嵌入子路由的,`Home.vue` 和 `NextPage.vue` 是这个例子中的两个页面。 可以简单的认为,在 `` 标签之外的内容都可以认为是 App Shell,不仅包括上面的 img 标签,下面的 `bottom-navigation` 组件,还包括在 `
` 标签外面的内容,如 `index.html` 文件中的其他部分。 ![Chapter02 demo App Shell 和主体内容示意图](./img/chapter02_demo_appshell.png) #### 如何缓存 App Shell 内容 在这个示例中,`service-worker.js` 文件是通过 SW Precache Webpack Plugin 插件生成的,可以参考 [sw-precache-webpack-plugin](https://github.com/goldhand/sw-precache-webpack-plugin#readme) 文档。代码如下所示: ```javascript // build/webpack.prod.js #13 行 // 引入 sw-precache-webpack-plugin 插件 const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin'); // build/webpack.prod.js #121 行 // 添加 sw-precache-webpack-plugin 插件生成 Service Worker 文件 new SWPrecacheWebpackPlugin(config.swPrecache.build) ``` 它的配置写在 `config/sw-preache.js` 文件中,开发者可以对它做一定的修改。 编译之后,会在 `dist` 目录中生成 `service-worker.js` 文件,第四章中有对 Service Worker 详细的介绍,在这里我们不必关心这个文件中过多的细节。插件生成的 Service Worker 做了很多事情,如下: * 列出了项目中静态文件列表 * 预缓存了 App Shell 的静态文件 * 具有预缓存动态更新机制 * Service Worker 的更新机制 ```javascript // 预缓存文件列表,其中包含 App Shell 所有的文件 var precacheConfig = [["/index.html","cb8786927330e5bdd417a47314a7300b"],["/static/css/app.5be76b7d213b43df9723e8ab15122efb.css","5b0aa7a24708300b7176f0304923ab39"],["/static/images/icons/icon_144x144.png","4bf0757895bd1cda44ee9204fe06a661"],["/static/images/icons/icon_152x152.png","c69908d502224c5317453f8ae725bbf0"],["/static/images/icons/icon_192x192.png","c69908d502224c5317453f8ae725bbf0"],["/static/images/icons/icon_512x512.png","8eaee831d59442821083453b174df0f6"],["/static/js/0.2dd5b370dbfb9afda866.js","5455b40e8991749d4aeb904f09389394"],["/static/js/1.f740f9813d49b7661238.js","65b7858bacef211f345d0d88fda81092"],["/static/js/app.e9b2b9e109561a7201f5.js","a28433e856b3e149bac022df934eb94e"],["/static/js/manifest.69fbe521a6bc723a8ce1.js","c6b6fbf7902cd5325b5b1d43045db622"],["/static/js/vendor.7bb72363822163e0f3b3.js","38d1baa4e38eeab57826462846900d69"],["/static/manifest.json","a521180656694782bc69a54ee5900dfb"]]; var cacheName = 'sw-precache-v3-sw-cache-chapter02-appshell-' + (self.registration ? self.registration.scope : ''); ``` ## 总结 总结来说,App Shell 把站点内容划分为“变”和“不变”两个部分,再辅以 Service Worker 技术将“不变”的部分缓存起来,以达成快速加载页面的效果。 通过 App Shell 和 Service Worker,我们有办法让“不变”的部分快速展现,那么针对“变”的部分,我们有什么办法让它展现得更快呢,或者让它“看起来”更快?在这里我们再提出另一种方案,它虽然不能在实质上提升页面的加载速度,但可以让它在感官上更快,它就是下一节要介绍的骨架屏。 ================================================ FILE: chapter02/4-app-skeleton.md ================================================ # 骨架屏 在前面的章节,我们说过,首屏速度对于用户留存率至关重要。 很多站点都会在完成基本功能后(或者同时)进行性能优化,常见的性能优化手段包括静态资源合并,压缩文件,使用 CDN,包括上一小节介绍的 App Shell 等,这些的确能够显著地缩短加载时间。但是我们想象一下,在首次打开时,主体内容渲染完成之前,页面基本上是空白的,这对于用户体验是非常不友好的。如果我们用能够快速渲染的静态图片/样式/色块进行占位,让用户对后续会渲染的内容有一定的预期,这要比白屏等待要好的多,这就是骨架屏。如上一节的结尾所述,它本质上并不提升页面的加载速度,却能提升感官体验,让网页“看起来”更快。 ## 骨架屏长什么样 您可能没听说过骨架屏这个名字,但应该很早就在其他 App 上看到过,骨架屏常见的种类有列表、图片和两者的混合。 下面的图是[饿了么 Web 站点](https://h5.ele.me/msite/) 骨架屏的效果,这种效果是比较常见的类型,它使用各种形状的色块来模拟图片和文字,有些图标也会使用圆形色块。为了追求效果,还可以在色块表面添加动画效果,如波纹,看起来就像具有 loading 效果的骨架屏。 ![饿了么 Web 站点骨架屏效果图](./img/eleme.png) 在图片居多的站点,使用低像素的图片进行占位也是一个不错的选择,图片配色及变化和原图相近即可,如果觉得生成低像素的图片成本较高,可以降级使用纯色色块代替,但色块的颜色最好和图片主体颜色相似,如果觉得这个成本还是很高,那么可以直接采用上面例子中统一颜色的色块。 ![低像素图片示例](./img/image_skeleton.png) 骨架屏的精髓,并不是用什么来占位,而是无论什么内容占位,一定要保持渲染前和渲染后结构相似,不能差距太大,最好保持色块/图片间距一样,避免页面渲染后内容跳动。 ## 骨架屏能用在哪里 现在的 Web 应用,从架构上来说分为前端渲染(CSR)和后端渲染(SSR)两种,骨架屏适用于前端渲染的页面,而后端渲染的页面渲染首屏时所有内容都已经存在了,因此无需骨架屏。但是,即使是后端渲染的页面有时也会存在前端渲染的区域,比如列表的加载,只要是用到 JavaScript 来渲染内容的地方,都可以选择性的使用骨架屏来占位。 由于近几年 Angular/React/Vue 的推出,前端渲染的站点越来越多,它们让开发复杂 Web App 变的很简单。它们的流行也大大提高了 App Shell 和骨架屏的普适性,大部分此类站点都可以采用这种模式。 ## 在 Vue 项目中使用骨架屏 我们以 Vue 开发的单页应用为例,在其它框架上,实现思路是一致的。在这个示例中,为了方便调试,笔者将 Service Worker 的注册逻辑删除了。 开发者可以下载示例代码并启动,如下所示: ```bash # 从 GitHub 下载代码到本地 pwa-book-demo 目录 $ git clone https://github.com/lavas-project/pwa-book-demo.git # 进入到 chapter02/appskeleton-01 目录 $ cd chapter02/appskeleton-01 # 安装 npm 依赖 $ npm install # 启动 chapter02 appskeleton-01 示例 $ npm run dev ``` 为了方便调试,我们将 `src/main.js` 中 Vue 实例的挂载时间推迟了 2s,在这期间,我们能看到骨架屏的效果。 ```javascript // src/main.js const app = new Vue({ router, components: { App }, template: '' }) router.onReady(() => { // 将 mount 时间延后 2s,便于查看效果 setTimeout(() => app.$mount('#app'), 2000) }) ``` 然后,我们需要在 `index.html` 中添加骨架屏,如下: ```html ...
``` 填写完成之后,刷新页面您能看到下图所示的骨架屏。 Chapter02 demo 骨架屏示意图 ## 进阶优化:更快的展现骨架屏 在上面的例子中,为了让骨架屏尽早展现,我们需要做到以下两点: 1. 把骨架屏的 HTML 内联在 `index.html` 中,而不是用 JavaScript 来渲染 2. 骨架屏的 CSS 最好内联,保证骨架屏在最短的时间内渲染 可惜的是,尽管我们这么做了,也不一定能取得想要的结果,通过实际测试会发现骨架屏很难被用户看到,或者说它展示的时间不如预期的早。 ### 现状分析 我们在上面的示例基础上来分析一下原因是什么。用 Chrome 调试工具中的 Performance 记录渲染流程,为了简单一些,我们可以将网络设置成 Slow 3G,会比较明显的发现骨架屏出现的时间非常短。 > 为了更接近生产环境,先将示例编译之后再调试。 ```bash # 在 chapter02/appskeleton-01 目录中 # 编译 $ npm run build # 进入到编译后的 dist 目录 $ cd dist # 安装静态文件调试服务 $ npm install -g edp # 在 dist 目录中启动本地静态文件服务 $ edp ws start ``` 打开 Chrome 浏览器访问 `http://localhost:8848`。下图是笔者用调试工具记录的页面渲染流程,发现页面第一次渲染是在 `app.*.css` 加载完成之后。 ![骨架屏渲染被样式文件加载阻塞](./img/appskeleton_performance.png) 按照预想,骨架屏应该在 HTML 加载完成之后立刻渲染出来,也就是在浏览器获取外链资源的同时,这也是为什么我们把骨架屏的 HTML 和样式都内联的原因,然而事与愿违,浏览器并不买账。 ### 浏览器做了什么 熟悉浏览器的开发者很快就能理解,这与浏览器的渲染顺序有关。 相信大家都整理过行李箱。在整理行李箱时,会根据每个行李的大小合理安排,大的和小的配合,填满一层再整理上面一层。如果突然有人跑来和你说电脑不用带了,要多带两件衣服,这时除了想打他之外,你还需要重新安排行李。在浏览器中,这个过程叫做重排(reflow),而那个馊主意就是新加载的样式文件。显而易见,重排的开销是很大的,需要尽力避免。 既然每个 CSS 文件都可能会触发重排和重绘,那索性等待 `` 中所有的外链样式文件加载完成之后再渲染,这个流程本身是没有问题的,却在骨架屏的应用中出了一些问题。 页面从加载到展现的大致顺序如下: 1. 加载 HTML 文件 2. 解析 DOM 3. 并行加载 CSS/JS 资源 4. 如果 `` 中存在外链的样式,则阻塞渲染等待样式文件加载并解析完成 5. 如果 `` 中存在外链的 script,则阻塞渲染等待 script 文件加载并执行完成 在 Vue 的项目编译完成之后,`` 标签中的结构如下,在 `` 标签中插入了一个外链的样式文件,导致骨架屏渲染推后。 ```html PWA Chapter02 Demo - App Skeleton ``` 为了尽早展现骨架屏,我们将骨架屏渲染所需的样式和 HTML 内联,却被页面中其他的外链样式文件阻塞了渲染。拿上一节中的 Vue 示例来举例,由于浏览器解析完 DOM 之后是并行加载外链资源的,所以在样式文件加载完成之后,JavaScript 文件也基本已经加载完成,因此在骨架屏真的渲染出来之后没多久就被 JavaScript 渲染的真正内容取代,这就是为什么骨架屏出现非常靠后,效果大打折扣。 所以,我们需要告诉浏览器,请放心大胆的先渲染骨架屏。 ### 避免样式文件的加载阻塞骨架屏的渲染 Webpack 编译的 Vue 项目,会在 `index.html` 的 `` 插入外链的样式文件,``,这无疑会阻塞骨架屏的渲染。浏览器还提供了预加载机制,使用方法非常简单,只需将 `rel="stylesheet"` 改为 `rel="preload"`,浏览器会在空闲的时候加载并缓存,之后再使用就不用重复加载。 这看似无关的技术,在骨架屏的应用里将起到很大的作用,因为**预加载的资源不会阻塞渲染**. 我们通过这种方式告诉浏览器,先不要管 `app.xxx.css`,直接渲染后续内容,在 `app.xxx.css` 文件加载完成之后,再将它重新设置为样式文件,如下代码所示: ```html ``` 方法的核心是通过改变 `rel` 让浏览器重新认定这个 `` 标签是样式文件,这样既不阻塞骨架屏的渲染,也能正常应用外链样式文件。 ### 这样就完了吗? 如果不将 `` 标签 `rel="stylesheet"` 改为 `rel="preload"`,浏览器会根据资源的书写顺序来顺序执行,即先应用外链样式,再执行外链 JavaScript 文件渲染主体内容。但是根据上面的步骤,我们使用预加载来加载样式文件,这样做的结果就是我们无法保证浏览器会先应用样式再运行 JavaScript 渲染内容,一旦 JavaScript 先执行并渲染出了内容,再应用外链样式,会导致页面重排和重绘,用户会先看到排版完全是乱的页面,再看到正常的页面,对用户体验是不小的伤害。 因此,我们还需要考虑到文件加载顺序的问题,在样式文件加载完成前,即使 JavaScript 已经渲染好了内容,也先不要替换掉骨架屏,等待样式文件加载完成后,再触发 JavaScript 进行挂载。 在 Vue 的项目中需要修改 `src/main.js` 和 `index.html` 文件。 ```javascript // src/main.js const app = new Vue({ router, components: { App }, template: '' }) /** * 挂载 Vue 渲染好的 HTML 元素到 #app 中,替换掉骨架屏 */ window.mount = function () { app.$mount('#app') } ``` ```html ``` 这样就好了吗? 还不够完善,如果样式文件在 JavaScript 文件之前加载,那么 `mount` 函数还没有声明,执行会出错,最终也导致主体内容没有渲染到页面上。还需要完善,增加一个标记告诉 JavaScript 样式是否加载完成,经过改造代码如下,同样还是 `src/main.js` 和 `index.html` 文件。 ```javascript // src/main.js const app = new Vue({ router, components: { App }, template: '' }) /** * 挂载 Vue 渲染好的 HTML 元素到 #app 中,替换掉骨架屏 */ window.mount = function () { app.$mount('#app') } // 如果样式文件已经加载完成了,直接挂载 if (window.STYLE_READY) { window.mount() } ``` ```html ``` 考虑到浏览器不支持 JavaScript 的情况,那么还需要增加一个 `