GitChat · 前端 | Webpack 工程的 PWA 实战
来源:互联网 发布:手机黄金交易软件 编辑:程序博客网 时间:2024/06/05 03:54
GitChat 作者:Jrain’L
原文:Webpack 工程的 PWA 实战
关注微信公众号:GitChat 技术杂谈 ,一本正经的讲技术
前言
在现代化的前端开发中,webpack已经成为了标配。与此同时,随着Google等一线互联网公司的大力推行,PWA(Progress Web Application,渐进式网络应用)的概念也逐渐进入了人们的视野。本文的内容,将会从搭建一个基于webpack的工程开始,介绍webpack相关的知识,同时引入PWA的概念,逐步把这个工程打造成一个完整的PWA应用,并最终部署上线。
在阅读这篇文章之前,要求读者了解webpack的基本概念,了解npm或者yarn的相关操作,这样在阅读的过程中才能把精力聚焦在重点的内容上。
项目构思
在大多数的前端应用当中,都喜欢拿TodoMVC(不是Hello World)作为例子。在这篇文章中,我们也通过构建一个TodoMVC来作为学习的例子。
todomvc.com这个网站提供了几乎所有框架/原生JS制作TodoMVC的例子,不同阵营的开发者都可以在这里找到自己想要的答案。为了简单起见,我将使用VueJS
来构建这一个TodoMVC。当然,本文并不针对某个具体框架进行详细讨论,仅仅用其作为一个趁手的工具去完成我们的目的。
这个TodoMVC具有以下几个功能:
能够添加一个todo;
能够修改这个todo的状态为“完成”或者“激活”;
页面能够按照“全部”、“完成”、“激活”来切换todo list的视图;
提供删除、清空todo的操作。
通过分析上面的几个功能,我们不难发现,这个项目理应以“模块化”的形式去组织代码,因此也顺理成章地需要引入webpack来帮我们处理模块化加载的问题。
目录结构
项目已经部署到了https://github.com/jrainlau/TodoMVC-PWA,其中项目的关键目录结构如下:
├── package.json├── favicons # PWA专用的静态资源├── index.html # 开发模式下的html文件├── index.tpl # 生产模式下的html模板├── src│ ├── App.vue # 根.vue文件│ ├── assets # 静态资源目录│ ├── components # 组件目录│ └── main.js # 入口js文件├── webpack.config.js # webpack配置文件
整个项目的核心代码均在/src
目录下,后面使用webpack所处理的文件也基本都是在这个目录里面。
进入main.js
文件,我们可以看到下列的基本代码:
import Vue from 'vue'import App from './App.vue'new Vue({ el: '.todoapp', render: h => h(App)})
这一段的代码的含义,就是注册一个Vue实例
,然后把根组件App.vue
挂载在class名为.todoapp
的html节点上。在接下来的章节里,对于这个应用的PWA改造,主要将会在这个关键的入口文件中进行。
Webpack配置
对于webpack的配置,有着多种多样的办法,比较主流的是把不同的功能区分成不同的配置文件,在运行的时候按条件指定。还有一种做法,就是在同一个配置文件内,按照不同的条件,通过module.exports
把对应的配置暴露出去。在本文的例子中,由于代码逻辑并不复杂,为了方便展示,使用的是第二种办法。
作为一个相对完整的项目,本文所示例子的webpack配置也区分了开发模式与生产模式,环境的区分通过往npm script
输入不同的参数实现。具体的webpack配置代码如下:
const path = require('path')const webpack = require('webpack')const cleanWebpackPlugin = require('clean-webpack-plugin')const htmlWebpackPlugin = require('html-webpack-plugin')const copyWebpackPlugin = require('copy-webpack-plugin')module.exports = { entry: './src/main.js', output: { path: path.resolve(__dirname, './dist'), filename: 'build.js' }, module: { rules: [ // ... ] }, devServer: { host: '0.0.0.0', historyApiFallback: true, noInfo: true }, performance: { hints: false }, devtool: '#eval-source-map'}if (process.env.NODE_ENV === 'production') { module.exports.devtool = '#source-map' module.exports.output = { path: path.resolve(__dirname, './docs'), filename: '[name].[hash].js' } module.exports.plugins = (module.exports.plugins || []).concat([ new webpack.DefinePlugin({ 'process.env': { NODE_ENV: '"production"' } }), // ... new htmlWebpackPlugin({ filename: 'index.html', template: 'index.tpl', inject: true, minify: { removeComments: true, collapseWhitespace: true, removeAttributeQuotes: true }, chunks: ['main'], chunksSortMode: 'dependency' }), new cleanWebpackPlugin(['docs']), new copyWebpackPlugin([ { from: 'favicons' } ])}
通过上述的配置项我们可以看到,生产环境和开发环境的配置大同小异,较大的区别有两处。
开发模式所指定的输出文件,文件名并没有加入
hash
。在开发模式下,文件会被构建到
dist
目录下,通过根目录下的index.html
运行;而生产模式,会使用html-webpack-plugin
,根据模板index.tpl
去**动态生成**html文件,然后被构建到docs
目录下。
为什么要这么做呢?其实是跟浏览器的缓存策略有关。为了保证每一次的构建都能及时生效,我们希望浏览器能够拉取最新的资源。而为文件名添加hash
值是一个行之有效的办法。然而,如果直接使用index.html
,需要手动指定所需加载的资源,而借助html-webpack-plugin
,我们就可以动态地注入带有hash的资源,省去了手动维护资源的麻烦。
在默认情况下,webpack会读取webpack.config.js
作为它的配置文件,所以我们可以打开package.json
,查看里面的npm script
命令,看看我们应该如何对这个项目进行构建:
"scripts": { "dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot", "build": "cross-env NODE_ENV=production webpack --progress --hide-modules"},
构建指令非常简单,通过cross-env
工具指定NODE_ENV
的值,即可区分不同的环境。
什么是PWA
在搞定一个“普通项目”之后,我们就可以对其进行PWA升级了。在此之前,我们有必要了解一下到底什么是PWA。
PWA全称Progress Web Application,也就是渐进式web应用,是由Google在2015年就提出的概念。它本质上是一个普通的web页面,但是由于使用了Service Worker、Notification/Push API、Manifest等新的技术,使得这个web页面能够拥有媲美原生APP的用户体验,比如离线访问、消息推送等。
PWA具有以下几个优点:
渐进式:能运行在所有的浏览器中,只要支持PWA的浏览器都能获得性能和体验上的提升,不支持的浏览器也能正常访问。
响应式:能够运行在所有的设备当中,响应式适配不同大小的屏幕。
允许离线访问:得益于Service Worker的缓存机制,PWA允许离线使用。
媲美原生APP:拥有和原生APP相似的图标、启动画面、全屏体验等。
更新及时:Service Worker的更新机制让PWA永远处于最新版而无需用户重新手动更新。
搜索引擎友好:由于符合W3C标准的Manifest的存在,使得搜索引擎可以方便地收录PWA。
交互性强:通过Notification API等消息推送功能,使得PWA与用户的交互性更强。
安装简单:只要通过浏览器“添加到主屏”的功能即可安装PWA,省去在App Store寻找APP的麻烦。
分享容易:由于PWA只是一个web网站,所以只需要分享一条URL就可以把这个PWA分享出去。
是不是觉得PWA非常强大,非常有研究价值呢?那么接下来,我们一起来看看它的几个核心知识点。
Service Worker
Service Worker,简单来说,是一个架设在“浏览器”与“服务器”中间的“代理”,它可以拦截浏览器与服务器的资源请求,同时对资源进行缓存。与Web Worker类似,它也是一个独立于浏览器的进程,能够在后台运行。
由于安全策略的问题,Service Worker只能运行在https协议,或者本地localhost/127.0.0.1开发域名下,所以要发布PWA的读者,需要把自己的网站升级成https。
学习Service Worker,最重要的一点是理解它的“生命周期”。我们可以通过两张图,直观地感受Service Worker的生命周期:
图片来源:
https://bitsofco.de/the-service-worker-lifecycle/?utm_source=javascriptweekly&utm_medium=email
图片来源:
https://github.com/delapuente/service-workers-101
INSTALLING 正在安装阶段
此阶段处于Service Worker被注册的时候。在这个阶段,Service Worker的install()
方法将会被执行,声明的资源即将被加入缓存。
INSTALLED 安装完成阶段
当Service Worker完成其初始化安装之后就会进入到这个阶段。在这个阶段,它是一个“有效但尚未激活的worker”,等待着客户端对其进行激活使用。我们可以在这个阶段告知用户,PWA已经可以进行升级了。
ACTIVATING 正在激活阶段
当客户端已无其它激活状态的Service Worker、或者脚本中的skipWaiting()
方法被调用、或者用户关闭了该Service Worker作用域下的所有页面时,就会触发ACTIVATING阶段。在这个阶段中,Service Worker脚本中的activate()
事件会被执行,此时可以清除掉过期的资源,并将进入下一个“激活完成阶段”。
ACTIVETED 激活完成阶段
此时Service Worker已经被激活并生效,可以开始控制网站的资源请求了。此时Service Worker内的fetch
和message
事件已经可以被监听。
REDUNDANT 废弃阶段
当安装失败,激活失败或者当前Service Worker被其他Service Worker替换时,就会进入这个阶段,此时Service Worker将失去对页面的控制。
值得注意的是,上面说的“生命周期”,并非指某个具体的事件,而是“在这个阶段有什么作用,可以调用什么方法”的意思。初学者很有可能会弄混“周期”与“事件”之间的关系。
更多关于Service Worker生命周期的内容,可以参考下面这篇文章:
https://bitsofco.de/the-service-worker-lifecycle/?utm_source=javascriptweekly&utm_medium=email
Manifest文件
前面提到,PWA具有和原生APP相似的用户体验,其中“添加到主屏”可以说是最关键的部分。和普通网页以书签形式添加到主屏不同,PWA在添加到主屏以后,可以做到和原生APP一样全屏展示,在任务管理器里面也是独立于浏览器进程的存在。同时它还允许开发者指定图标、指定APP名和指定启动画面——这一切都需要Manifest文件的支持。
Manifest是一个普通的json
文件,它位于网站的根目录下,里面声明了这个PWA的“基本信息”,一个典型的manifest.json
文件如下:
{ "name": "Todo", "icons": [ { "src": "android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } ], "theme_color": "#ffffff", "background_color": "#ffffff", "start_url": "/TodoMVC-PWA/", "display": "standalone"}
在这个manifest.json
文件中,我们可以轻松得到这个PWA的信息:
name:定义此PWA的名称。
icons:定义一系列的图标以适应不同型号的设备。
theme_color:主题颜色(影响手机状态栏颜色)。
background_color:背景颜色。
start_url:启动地址。由于PWA实际上是一个web页面,所以需要定义PWA在启动时应该访问哪个地址。
display:“standalone”表示其以类似原生APP的全屏方式启动。
在定义好manifest.json
文件后,我们可以通过Chrome的开发者工具看到详细的内容:
下面几个网站都可以在线帮我们生成想要的manifest.json
文件,非常方便:
https://app-manifest.firebaseapp.com/
https://tomitm.github.io/appmanifest/
https://brucelawson.github.io/manifest/
到目前为止,iOS并未支持PWA,但我们可以通过几个由苹果公司提供的html标签使web页面获得和原生应用类似的效果。
应用图标:<link rel="apple-touch-icon" href=“apple-touch-icon.png">
启动画面:<link rel="apple-touch-startup-image" href="launch.png">
应用名称:<meta name="apple-mobile-web-app-title" content="Todo-PWA">
全屏效果:<meta name="apple-mobile-web-app-capable" content="yes">
设置状态栏颜色:<meta name="apple-mobile-web-app-status-bar-style" content="#fff">
至此,PWA已经适配了Android和iOS系统,分别使用Chrome和Safari浏览器的“添加到主屏”功能即可。感兴趣的读者可以访问下面这个地址进行体验:
https://jrainlau.github.io/TodoMVC-PWA/
App Shell
如果打开一个大型的web页面,“白屏”的体验是非常糟糕的。在PWA中,我们可以通过App Shell的办法优化这个问题。
App Shell,顾名思义,就是“壳”的意思,也可以理解为“骨架屏”,说白了就是在内容尚未加载完全的时候,优先展示页面的结构、占位图、主题和背景颜色等,它们都是一些被强缓存的html,css和javascript。
要用好App Shell,就必须保证这部分的资源被Service Worker缓存起来。我们在组织代码的时候,可以优先完成App Shell的部分,然后把这部分代码分别打包构建出来。
使用Offine-Plugin把网站升级成PWA
前面铺垫了那么多,终于要进行实际的操作了。由于是借助webpack构建的项目,因此我们可以很方便地使用这个名叫Offine-Plugin
的webpack插件帮助我们升级PWA。
PWA的核心可谓是Service Worker,任何一个PWA都有且只有一个service-worker.js
文件,用于为Service Worker添加资源列表,进行注册、激活等生命周期操作。但是在webpack构建的项目中,生成一个service-worker.js
可能会面临两个较大的问题:
webpack生成的资源多会生成一串hash,Service Worker的资源列表里面需要同步更新这些带hash的资源;
每次更新代码,都需要通过更新
service-worker.js
文件版本号来通知客户端对所缓存的资源进行更新。(其实只要这一次的service-worker.js
代码和上一次的service-worker.js
代码不一样即可触发更新,但使用明确的版本号会更加合适)。
相比与Service Worker-precache-webpack-plugin
,个人认为offline-plugin
具有如下优点:
更多的可选配置项,满足更加细致的配置要求;
更为详细的文档和例子;
更新频率相对更高,star数更多;
自动处理生命周期,用户无需纠结生命周期的坑;
接下来将会介绍这个插件的用法。
基本使用
安装
npm install offline-plugin [--save-dev]
初始化
第一步,进入webpack.config:
// webpack.config.js examplevar OfflinePlugin = require('offline-plugin');module.exports = { // ... plugins: [ // ... other plugins // it's always better if OfflinePlugin is the last plugin added new OfflinePlugin() ] // ...}
第二步,把runtime添加到你的入口js文件当中:
require('offline-plugin/runtime').install();ES6/Babel/TypeScriptimport * as OfflinePluginRuntime from 'offline-plugin/runtime';OfflinePluginRuntime.install();
经过上面的步骤,offline-plugin已经集成到项目之中,通过webpack构建即可。
配置
前面说过,offline-plugin
支持细致的配置,以满足不同的需求。下面将介绍几个比较常用的配置项,方便大家进一步使用。
Caches: 'all' | Object
告诉插件应该缓存什么东西,并以何种方式进行缓存。
all
: 意味着所有webpack构建出来的资源,以及在externals
选项中的资源都会被缓存。Object
: 包含三个数组或正则的配置对象(main
,additional
,optional
),它们都是可选的,且默认为空。默认:
all
。externals: Array<string>
允许开发者指定一些外部资源(比如CDN引用,或者不是通过webpack生成的资源)。配合
Caches
的additional
项,能够实现缓存外部资源的功能。默认:
null
。举例:
['fonts/roboto.woff']
。ServiceWorker: Object | null | false
该对象包含多个配置项,这里仅列举最常用的。
events
:布尔值。允许runtime接受来自Service Worker的消息,默认值为false。navigateFallbackURL
:当一个URL请求从缓存或网络都无法被获取时,将会重定向到该选项所指向的URL。AppCache: Object | null | false
offline-plugin
默认支持AppCache
,但是AppCache
草案已经被web标准所废弃,不建议使用。但是由于仍然有部分浏览器支持,所以插件默认提供这个功能。
runtime
上一节介绍了offline-plugin
在webpack当中的配置,这一节将介绍runtime的一些用法。
若要使offline-plugin
生效,用户必须在入口js文件中通过runtime进行初始化操作:
// 通过AMD方式require('offline-plugin/runtime').install();// 或者通过ES6/Babel/TypeScript方式import * as OfflinePluginRuntime from 'offline-plugin/runtime';OfflinePluginRuntime.install();
OfflinePluginRuntime
对象提供了下列三个方法:
install(options: Object)
开启ServiceWorker/AppCache的安装流程。这个方法是安全的,并且必须在页面初始化的时候就被调用。另外请勿把它放在任何的条件语句之内。(这句话不全对,在后面的降级方案里面会详细介绍)
applyUpdate()
接受当前所安装的Service Worker的更新信息。
update()
检查新版本的ServiceWorker/AppCache的更新信息。
runtime.install()
方法接受一个配置对象参数,用于处理Service Worker各个生命周期里面的事件。onInstalled
当ServiceWorker/AppCache被install时执行,可用于展示“APP已经支持离线访问”。
onUpdating
AppCache不支持该方法。
当更新信息被获取且浏览器正在进行资源更新时触发。在这个时刻,一些资源正在被下载。
onUpdateReady
当
onUpdating
事件完成时触发。这时,所有资源都已经下载完毕。通过调用
runtime.applyUpdate()
方法来触发更新。onUpdateFailed
当
onUpdating
事件因为某些原因失败时触发。这时没有任何资源被下载,同时所有的资源更新进程都应该被取消或跳过。
onUpdated
当更新被接受时触发。
降级方案
当某些时候我们需要撤掉Service Worker进行降级的时候,我们需要主动注销Service Worker。然而offline-plugin
默认没有提供注销Service Worker的unregister()
方法,所以我们需要自己实现。
其实要主动注销Service Worker非常简单,我们可以直接调用ServiceWorkerContainer.getRegistrations()
方法来拿到registration
实例,然后调用registration.unregister()
方法即可,具体代码如下:
if ('serviceWorker' in navigator) { navigator.serviceWorker.getRegistration().then((registration) => { registration && registration.unregister().then((boolean) => { boolean ? alert('注销成功') : alert('注销失败') }); })}
在调用该方法后,Service Worker已经被注销,刷新一下页面就能看到资源是重新从网络获取的了。
在真实的生产环境中,我们可以通过调用接口,来决定是否使用降级方案:
fetch(URL).then((Service Workeritch) => { if (Service Workeritch) { OfflinePluginRuntime.install() } else { if ('serviceWorker' in navigator) { navigator.serviceWorker.getRegistration().then((registration) => { registration && registration.unregister().then((boolean) => { boolean ? alert('注销成功') : alert('注销失败') }) }) } }})
尾声
由于国内众所周知的原因,依赖Google服务的Notification API
无法使用,所以关于PWA消息推送这一块暂时未付诸实践,待将来有时间再进行深入研究。
PWA是前端的未来,也是已经到达的未来。非常期待各位读者自行尝试,与我一起交流探讨,共同推进PWA在国内的发展。
彩蛋
重磅 Chat 分享:《如何在三年内快速成长为一名技术专家》
分享人:
方腾飞 并发编程网创始人,支付宝架构师Chat简介:
工作前三年是职业生涯中成长最快的几年,在这段时间里你会充满激情,做事专注,也容易养成良好的习惯。
在我们公司有些同学在前三年中就快速成为某一个领域的技术专家,有些同学也可能止步不前。本场Chat和大家一起探讨下如何在三年内快速成长为一名技术专家。
学习方法:
掌握良好的学习心态 掌握系统化的学习方法
知识如何内化成能力
广度和深度的选择
实战技巧:
你需要学会的编码习惯 如何在普通项目中提高自己的能力
在业务团队做
引用文字
开发如何成长
想要免费参与本场 Chat ?
很简单,公众号后台回复「技术专家」
- GitChat · 前端 | Webpack 工程的 PWA 实战
- GitChat · 前端 | JavaScript 进阶之 Vue.js + Node.js 入门实战开发
- 基于webpack的前端工程化开发解决方案探所
- GitChat · 架构 | 大规模私有云产品自动升级的架构选型和实战
- 基于webpack搭建前端工程解决方案探索
- 基于webpack搭建前端工程解决方案探索
- 基于webpack搭建前端工程解决方案探索
- 基于webpack搭建前端工程解决方案探索
- 基于 webpack 搭建前端工程基础篇
- webpack gulp 实现完整前端工程化
- 基于webpack搭建前端工程解决方案探索
- 基于webpack搭建前端工程解决方案探索
- webpack构建前端工程路径问题
- 基于 webpack 搭建前端工程基础篇
- GitChat·前端 | Vue 组件库实践和设计
- GitChat · 前端 | React 生态系统:从小白到大神
- 腾讯Web前端大会 企鹅电竞PWA实战(MR_LP)
- 基于webpack的前端工程化开发之多页站点篇(一)
- CF854B Maxim Buys an Apartment【思路】
- 三个实例演示 Java Thread Dump 日志分析
- CodeForces
- JanusGraph
- 复习-数据结构之线性表
- GitChat · 前端 | Webpack 工程的 PWA 实战
- C# 三层架构之系统的登录验证与添加数据的实现
- Struts2 文件上传
- redis-cluster配置攻略 本人亲自尝试
- 去除PDF的水印【9种方法总结】
- up
- 复习知识
- 基于vue-cli的vue项目之axios的使用3--get传参请求
- 剑指offer-21:包含min函数的栈