8000 feat: support some PWA features by xiaoiver · Pull Request #1307 · umijs/umi · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

feat: support some PWA features #1307

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Oct 25, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 51 additions & 1 deletion docs/plugin/umi-plugin-react.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,57 @@ Open webpack cache with [hard-source-webpack-plugin](https://github.com/mzgoddar

* Type `Object`

Enable pwa 。
Enable some PWA features including:

* Generate a `manifest.json`
* Generate a Service Worker on `PRODUCTION` mode

options include:

* `manifestOptions` Type: `Object`, includes following options:
* `srcPath` path of manifest, Type: `String`, Default `src/manifest.json`
* `workboxPluginMode` Workbox mode, Type: `String`, Default `GenerateSW`(generate a brand new Service Worker); or `InjectManifest`(inject code to existed Service Worker)
* `workboxOptions` Workbox [Config](https://developers.google.com/web/tools/workbox/modules/workbox-webpack-plugin#full_generatesw_config),some important options:
* `swSrc` Type: `String`, Default `src/manifest.json`, only in `InjectManifest` mode
* `importWorkboxFrom` Type: `String`,Workbox loads from Google CDN by default, you can choose to `'local'` mode which will let Workbox loads from local copies

You can refer to [Workbox](https://developers.google.com/web/tools/workbox/) for more API usages.

Here's a simple example:

```js
// .umirc.js or config/config.js
export default {
pwa: {
manifestOptions: {
srcPath: 'path/to/manifest.webmanifest')
},
workboxPluginMode: 'InjectManifest',
workboxOptions: {
importWorkboxFrom: 'local',
swSrc: 'path/to/service-worker.js')
}
}
}
```

You can also listen to some `CustomEvent` when Service Worker has updated old contents in cache.
It's the perfect time to display some message like "New content is available; please refresh.".
For example, you can listen to `sw.updated` event in such UI component:

```js
window.addEventListener('sw.updated', () => {
// show message
});
```

You can also react to network environment changes, such as offline/online:

```js
window.addEventListener('sw.offline', () => {
// make some components gray
});
```

### hd

Expand Down
48 changes: 47 additions & 1 deletion docs/zh/plugin/umi-plugin-react.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,53 @@ export default {

* 类型:`Object`

开启 pwa 。
开启 PWA 相关功能,包括:
* 生成 `manifest.json`
* 在 `PRODUCTION` 模式下生成 Service Worker

配置项包含:
* `manifestOptions` 类型:`Object`,包含如下属性:
* `srcPath` manifest 的文件路径,类型:`String`,默认值为 `src/manifest.json`(如果 `src` 不存在,为项目根目录)
* `workboxPluginMode` Workbox 模式,类型:`String`,默认值为 `GenerateSW` 即生成全新 Service Worker ;也可选填 `InjectManifest` 即向已有 Service Worker 注入代码,适合需要配置复杂缓存规则的场景
* `workboxOptions` Workbox [配置对象](https://developers.google.com/web/tools/workbox/modules/workbox-webpack-plugin#full_generatesw_config),其中部分重要属性如下:
* `swSrc` 类型:`String`,默认值为 `src/manifest.json`,只有选择了 `InjectManifest` 模式才需要配置
* `importWorkboxFrom` 类型:`String`,默认从 Google CDN 加载 Workbox 代码,可选值 `'local'` 适合国内无法访问的环境

更多关于 Workbox 的使用可以参考[官方文档](https://developers.google.com/web/tools/workbox/)。

一个完整示例如下:

```js
// .umirc.js or config/config.js
export default {
pwa: {
manifestOptions: {
srcPath: 'path/to/manifest.webmanifest')
},
workboxPluginMode: 'InjectManifest',
workboxOptions: {
importWorkboxFrom: 'local',
swSrc: 'path/to/service-worker.js')
}
}
}
```

当 Service Worker 发生更新以及网络断开时,会触发相应的 `CustomEvent`。
例如当 Service Worker 完成更新时,通常应用会引导用户手动刷新页面,在组件中可以监听 `sw.updated` 事件:

```js
window.addEventListener('sw.updated', () => {
// 弹出提示,引导用户刷新页面
});
```

另外,当网络环境发生改变时,也可以给予用户显式反馈:
```js
window.addEventListener('sw.offline', () => {
// 置灰某些组件
});
```

### hd

Expand Down
4 changes: 3 additions & 1 deletion packages/umi-build-dev/src/plugins/commands/build/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ export default function(api) {
const HtmlGeneratorPlugin = require('../getHtmlGeneratorPlugin').default(
service,
);
service.webpackConfig.plugins.push(new HtmlGeneratorPlugin());
// move html-webpack-plugin to the head, so that other plugins (like workbox-webpack-plugin)
// which listen to `emit` event can detect assets
service.webpackConfig.plugins.unshift(new HtmlGeneratorPlugin());
}

require('af-webpack/build').default({
Expand Down
59 changes: 59 additions & 0 deletions packages/umi-plugin-react/src/plugins/pwa/WebManifestPlugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { readFileSync } from 'fs';
import { join } from 'path';
import { prependPublicPath, PWACOMPAT_PATH } from './generateWebManifest';

export default class WebManifestPlugin {
constructor(options) {
this.options = options;
}

apply(compiler) {
const { publicPath, srcPath, outputPath, pkgName } = this.options;
// our default manifest
let rawManifest = {
name: pkgName,
short_name: pkgName,
display: 'fullscreen',
scope: '/',
start_url: './?homescreen=true',
orientation: 'portrait',
};

compiler.hooks.emit.tap('generate-webmanifest', compilation => {
if (srcPath) {
try {
rawManifest = JSON.parse(readFileSync(srcPath, 'utf8'));
} catch (e) {
compilation.errors.push(
new Error(
`Please check ${srcPath}, a WebManifest should be a valid JSON file.`,
),
);
return;
}
}

rawManifest.icons &&
rawManifest.icons.forEach(icon => {
icon.src = prependPublicPath(publicPath, icon.src);
});

// write manifest & pwacompat.js to filesystem
[
{
path: outputPath,
content: JSON.stringify(rawManifest),
},
{
path: PWACOMPAT_PATH,
content: readFileSync(join(__dirname, PWACOMPAT_PATH)),
},
].forEach(({ path, content }) => {
compilation.assets[path] = {
source: () => content,
size: () => content.length,
};
});
});
}
}
63 changes: 63 additions & 0 deletions packages/umi-plugin-react/src/plugins/pwa/generateWebManifest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { existsSync } from 'fs';
import { basename, join } from 'path';
import { resolve } from 'url';

export const PWACOMPAT_PATH = 'pwacompat.min.js';
export const DEFAULT_MANIFEST_FILENAME = 'manifest.json';

export function prependPublicPath(publicPath = '/', src) {
return resolve(publicPath, src);
}

export default function generateWebManifest(api, options) {
const {
config: { publicPath },
log,
paths: { absSrcPath },
addHTMLLink,
addHTMLHeadScript,
addPageWatcher,
onGenerateFiles,
} = api;

const defaultWebManifestOptions = {
srcPath: join(absSrcPath, DEFAULT_MANIFEST_FILENAME),
};
let { srcPath } = {
...defaultWebManifestOptions,
...options,
};
let manifestFilename = basename(srcPath);

if (existsSync(srcPath)) {
// watch manifest on DEV mode
if (process.env.NODE_ENV === 'development') {
addPageWatcher([srcPath]);
}
} else {
onGenerateFiles(() => {
log.warn(`You'd better provide a WebManifest. Try to:
1. Create one under: \`${srcPath}\`,
2. Or override its path with \`pwa.manifestOptions.srcPath\` in umi config`);
});
srcPath = null;
manifestFilename = DEFAULT_MANIFEST_FILENAME;
}

// add <link rel="manifest">
addHTMLLink({
rel: 'manifest',
href: prependPublicPath(publicPath, manifestFilename),
});

// use PWACompat(https://github.com/GoogleChromeLabs/pwacompat) for non-compliant browsers
addHTMLHeadScript({
async: '',
src: prependPublicPath(publicPath, PWACOMPAT_PATH),
});

return {
srcPath,
outputPath: manifestFilename,
};
}
41 changes: 34 additions & 7 deletions packages/umi-plugin-react/src/plugins/pwa/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,38 @@ import { join } from 'path';
import assert from 'assert';
import chalk from 'chalk';
import workboxWebpackPlugin from 'workbox-webpack-plugin';
import WebManifestPlugin from './WebManifestPlugin';
import generateWebManifest from './generateWebManifest';

export default function(api, options) {
const { pkg, relativeToTmp } = api;
const {
pkg,
relativeToTmp,
config: { publicPath },
paths: { absSrcPath },
} = api;
assert(
pkg && pkg.name,
`You must have ${chalk.underline.cyan(
'package.json',
)} and configure ${chalk.underline.cyan('name')} in it when enable pwa.`,
);

// generate webmanifest before workbox generation, so that webmanifest can be added to precached list
const { srcPath, outputPath } = generateWebManifest(api, {
...options.manifestOptions,
});
api.chainWebpackConfig(webpackConfig => {
webpackConfig.plugin('webmanifest').use(WebManifestPlugin, [
{
publicPath,
srcPath,
outputPath,
pkgName: pkg.name,
},
]);
});

if (process.env.NODE_ENV === 'production') {
api.addEntryCode(
`
Expand All @@ -24,22 +46,27 @@ require('${relativeToTmp(join(__dirname, './registerServiceWorker.js'))}');
mode === 'GenerateSW'
? {
cacheId: pkg.name,
skipWaiting: true,
clientsClaim: true,
}
: {};
const config = {
exclude: [/\.map$/, /favicon\.ico$/, /manifest\.json$/],
: {
swSrc: join(absSrcPath, 'service-worker.js'),
};
const workboxConfig = {
// remove manifest.json from exclude list. https://github.com/GoogleChrome/workbox/issues/1665
exclude: [/\.map$/, /favicon\.ico$/, /^manifest.*\.js?$/],
...defaultGenerateSWOptions,
...(options.workboxOptions || {}),
};

api.chainWebpackConfig(webpackConfig => {
webpackConfig.plugin('workbox').use(workboxWebpackPlugin[mode], [config]);
webpackConfig
.plugin('workbox')
.use(workboxWebpackPlugin[mode], [workboxConfig]);
webpackConfig.resolve.alias.set(
'register-service-worker',
require.resolve('register-service-worker'),
);
});
}

// TODO: 更新 html 文件
}
Loading
0