10

我正在尝试为我的应用构建 PWA;并且花了将近 48 小时试图弄清楚如何使用 Workbox 和 Laravel Mix。具有讽刺意味的是,谷歌说 Workbox 是为了让事情变得简单!

呸!

好吧,到目前为止我已经想通了——

  1. 我需要使用,InjectManifest Plugin因为我想在我的 Service Worker 中集成推送通知服务

  2. 我不知道如何为swSrcand指定路径swDest

  3. 什么代码应该进入我的webpack.mix.js,我是否应该在我的文件夹中有一个临时服务工作者来在resources/js文件夹中创建一个新的服务工作者public/

有人可以帮忙吗?

PS:我几乎阅读了所有的博客和帮助文章;但没有人谈到可靠地使用 Workbox 和 Laravel 混合。非常感谢这里的一些帮助。

4

1 回答 1

10

我最近对此进行了大量研究,虽然这可能不是您问题的完整答案,但它应该为您或访问此页面的任何其他人提供足够的指导以开始...

当我学习和研究更多时,我会添加到这个答案中。

出于此答案的目的,我将假设您的服务人员被称为service-worker.js,但是,您显然可以随意称呼它。

第 1 步 - Laravel 混合

假设你在你的项目中使用动态导入(如果你没有,你应该使用),你需要将 Laravel Mix 降级到版本 3。在 Laravel Mix 4 中有一个公认的错误会阻止 CSS 正确捆绑,这将在 Webpack 5 发布之前不会修复。

此外,此答案中概述的步骤是专门为 Laravel Mix 3 配置的。

第 2 步 - 导入或 ImportScripts

要解决的第二个问题是是否使用workbox-webpack-plugin注入workbox全局 usingimportScripts或是否应该禁用它(using importWorkboxFrom: 'disabled')并仅单独导入所需的特定模块......

该文档指出:

使用 JavaScript 捆绑器时,您不需要(实际上也不应该使用)workbox全局或workbox-sw模块,因为您可以直接导入各个包文件。

这意味着我们应该使用import而不是注入importScripts.

但是,这里存在各种问题:

  • 我们不希望service-worker.js包含在构建清单中,因为这将被注入到预缓存清单中
  • 我们不希望service-worker.js在 ie 中进行版本控制,production即名称应该始终是service-worker.js,而不是service-worker.123abc.js
  • InjectManifest将无法注入清单,因为该service-worker.js文件在运行时将不存在。

因此,为了使用import而不是importScripts,我们必须有两个单独的 webpack(混合)配置(有关如何执行此操作的指导,请参见结论)。我不是 100% 确定这是正确的,但是一旦我收到以下任一问题的答案,我会更新我的答案(请支持他们以增加收到答案的机会):

第 3 步 - 文件结构

假设您正在使用InjectManifest而不是GenerateSW,您将需要编写自己的服务工作者,该服务工作者将在每次构建时通过 webpack 插件将 JS 清单注入其中。这很简单,意味着您需要在源目录中创建一个文件,该文件将用作 service worker。

我的位于src/js/service-worker.js(如果您正在构建完整的 Laravel 项目,这将有所不同,我只是在独立应用程序中使用 Laravel Mix)

第 4 步 - 注册 Service Worker

有多种方法可以做到这一点;有些人喜欢将内联 JS 注入 HTML 模板,但其他人,包括我自己,只需在其app.js. 无论哪种方式,代码都应该类似于以下内容:

if ('serviceWorker' in navigator) {
    window.addEventListener('load', function() {
        navigator.serviceWorker.register('/service-worker.js');
    });
}

第 5 步 - 编写 Service Worker;workbox全局或模块导入

正如文档前面的引用中提到的,鼓励将特别需要的模块导入您的服务工作者,而不是使用workbox全局或workbox-sw模块。

有关如何使用各个模块以及如何实际编写 Service Worker 的更多信息,请参阅以下文档:

https://developers.google.com/web/tools/workbox/guides/using-bundlers

结论

根据我的所有研究(仍在进行中),我采用了以下概述的方法。

在阅读之前,请记住这是为独立的静态 PWA 配置的(即不是完整的 Laravel 项目)。

/src/service-worker.js(服务人员)

使用诸如 之类的捆绑webpack器时,建议使用import以确保仅包含必要的workbox模块。这是我的服务工作者骨架:

import config from '~/config'; // This is where I store project based configurations
import { setCacheNameDetails } from 'workbox-core';
import { precacheAndRoute } from 'workbox-precaching';
import { registerNavigationRoute } from 'workbox-routing';

// Set the cache details
setCacheNameDetails({
    prefix: config.app.name.replace(/\s+/g, '-').toLowerCase(),
    suffix: config.app.version,
    precache: 'precache',
    runtime: 'runtime',
    googleAnalytics: 'ga'
});

// Load the assets to be precached
precacheAndRoute(self.__precacheManifest);

// Ensure all requests are routed to index.html (SPA)
registerNavigationRoute('/index.html');

/package.json

拆分混合配置

"scripts": {  
  "development": "npm run dev-service-worker && npm run dev-core",  
  "dev": "npm run development",  
  "dev-service-worker": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js --env.mixfile=service-worker.mix",  
  "dev-core": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js --env.mixfile=core.mix",  
  "watch": "npm run dev-core -- --watch",  
  "watch-poll": "npm run watch -- --watch-poll",  
  "production": "npm run prod-service-worker && npm run prod-core",  
  "prod": "npm run production",  
  "prod-service-worker": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js --env.mixfile=service-worker.mix",  
  "prod-core": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js --env.mixfile=core.mix"  
}

命令说明

  • 所有标准命令都将以与通常相同的方式工作(即npm run dev等)。请参阅有关的已知问题npm run watch
  • npm run <environment>-service-worker将仅在指定环境中构建服务工作者
  • npm run <environment>-core将仅在指定环境中构建核心应用程序

已知的问题

  • 如果您使用的是利用 webpack 清单的 html 模板,那么您可能会遇到npm run watch. 到目前为止,我一直无法让它正常工作

降级到 Laravel Mix 3

"devDependencies": {  
    "laravel-mix": "^3.0.0"  
}

这也可以通过运行来实现npm install laravel-mix@3.0.0

/static/index.ejs

此 HTML 模板用于生成单页应用程序index.html。这个模板依赖于被注入的 webpack 清单。

<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" class="no-js">
    <head>

        <!-- General meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <meta name="description" content="<%= config.meta.description %>">
        <meta name="rating" content="General">
        <meta name="author" content="Sine Macula">
        <meta name="robots" content="index, follow">
        <meta name="format-detection" content="telephone=no">

        <!-- Preconnect and prefetch urls -->
        <link rel="preconnect" href="<%= config.api.url %>" crossorigin>
        <link rel="dns-prefetch" href="<%= config.api.url %>">

        <!-- Theme Colour -->
        <meta name="theme-color" content="<%= config.meta.theme %>">

        <!-- General link tags -->
        <link rel="canonical" href="<%= config.app.url %>">

        <!-- Manifest JSON -->
        <link rel="manifest" href="<%= StaticAsset('/manifest.json') %>" crossorigin>


        <!-- ----------------------------------------------------------------------
        ---- Icon Tags
        ---- ----------------------------------------------------------------------
        ----
        ---- The following will set up the favicons and the apple touch icons to be
        ---- used when adding the app to the homescreen of an iPhone, and to
        ---- display in the head of the browser.
        ----
        ---->
        <!--[if IE]>
            <link rel="shortcut icon" href="<%= StaticAsset('/favicon.ico') %>">
        <![endif]-->
        <link rel="apple-touch-icon" sizes="72x72" href="<%= StaticAsset('/apple-touch-icon-72x72.png') %>">
        <link rel="apple-touch-icon" sizes="120x120" href="<%= StaticAsset('/apple-touch-icon-120x120.png') %>">
        <link rel="apple-touch-icon" sizes="180x180" href="<%= StaticAsset('/apple-touch-icon-180x180.png') %>">
        <link rel="icon" type="image/png" sizes="16x16" href="<%= StaticAsset('/favicon-16x16.png') %>">
        <link rel="icon" type="image/png" sizes="32x32" href="<%= StaticAsset('/favicon-32x32.png') %>">
        <link rel="icon" type="image/png" sizes="192x192"  href="<%= StaticAsset('/android-chrome-192x192.png') %>">
        <link rel="icon" type="image/png" sizes="194x194"  href="<%= StaticAsset('/favicon-194x194.png') %>">
        <link rel="mask-icon" href="<%= StaticAsset('/safari-pinned-tab.svg') %>" color="<%= config.meta.theme %>">
        <meta name="msapplication-TileImage" content="<%= StaticAsset('/mstile-144x144.png') %>">
        <meta name="msapplication-TileColor" content="<%= config.meta.theme %>">


        <!-- ----------------------------------------------------------------------
        ---- Launch Images
        ---- ----------------------------------------------------------------------
        ----
        ---- Define the launch 'splash' screen images to be used on iOS.
        ----
        ---->
        <link rel="apple-touch-startup-image" href="<%= StaticAsset('/assets/images/misc/splash-640x1136.png') %>" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
        <link rel="apple-touch-startup-image" href="<%= StaticAsset('/assets/images/misc/splash-750x1294.png') %>" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
        <link rel="apple-touch-startup-image" href="<%= StaticAsset('/assets/images/misc/splash-1242x2148.png') %>" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
        <link rel="apple-touch-startup-image" href="<%= StaticAsset('/assets/images/misc/splash-1125x2436.png') %>" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
        <link rel="apple-touch-startup-image" href="<%= StaticAsset('/assets/images/misc/splash-1536x2048.png') %>" media="(min-device-width: 768px) and (max-device-width: 1024px) and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait)">
        <link rel="apple-touch-startup-image" href="<%= StaticAsset('/assets/images/misc/splash-1668x2224.png') %>" media="(min-device-width: 834px) and (max-device-width: 834px) and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait)">
        <link rel="apple-touch-startup-image" href="<%= StaticAsset('/assets/images/misc/splash-2048x2732.png') %>" media="(min-device-width: 1024px) and (max-device-width: 1024px) and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait)">


        <!-- ----------------------------------------------------------------------
        ---- Application Tags
        ---- ----------------------------------------------------------------------
        ----
        ---- Define the application specific tags.
        ----
        ---->
        <meta name="application-name" content="<%= config.app.name %>">
        <meta name="apple-mobile-web-app-title" content="<%= config.app.name %>">
        <meta name="apple-mobile-web-app-capable" content="yes">
        <meta name="apple-mobile-web-app-status-bar-style" content="<%= config.app.status_bar %>">
        <meta name="mobile-web-app-capable" content="yes">
        <meta name="full-screen" content="yes">
        <meta name="browsermode" content="application">


        <!-- ----------------------------------------------------------------------
        ---- Social Media and Open Graph Tags
        ---- ----------------------------------------------------------------------
        ----
        ---- The following will create objects for social media sites to read when
        ---- scraping the site.
        ----
        ---->

        <!-- Open Graph -->
        <meta property="og:site_name" content="<%= config.app.name %>">
        <meta property="og:url" content="<%= config.app.url %>">
        <meta property="og:type" content="website">
        <meta property="og:title" content="<%= config.meta.title %>">
        <meta property="og:description" content="<%= config.meta.description %>">
        <meta property="og:image" content="<%= StaticAsset('/assets/images/brand/social-1200x630.jpg') %>">

        <!-- Twitter -->
        <meta name="twitter:card" content="app">
        <meta name="twitter:site" content="<%= config.app.name %>">
        <meta name="twitter:title" content="<%= config.meta.title %>">
        <meta name="twitter:description" content="<%= config.meta.description %>">
        <meta name="twitter:image" content="<%= StaticAsset('/assets/images/brand/social-440x220.jpg') %>">


        <!-- ----------------------------------------------------------------------
        ---- JSON Linked Data
        ---- ----------------------------------------------------------------------
        ----
        ---- This will link the website to its associated social media page. This
        ---- adds to the credibility of the website as it allows search engines to
        ---- determine the following of the company via social media
        ----
        ---->
        <script type="application/ld+json">
            {
                "@context": "http://schema.org",
                "@type": "Organization",
                "name": "<%= config.company.name %>",
                "url": "<%= config.app.url %>",
                "sameAs": [<%= '"' + Object.values(config.company.social).map(x => x.url).join('","') + '"' %>]
            }
        </script>

        <!-- Define the page title -->
        <title><%= config.meta.title %></title>

        <!-- Generate the prefetch/preload links -->
        <% webpack.chunks.slice().reverse().forEach(chunk => { %>
            <% chunk.files.forEach(file => { %>
                <% if (file.match(/\.(js|css)$/)) { %>
                    <link rel="<%= chunk.initial ? 'preload' : 'prefetch' %>" href="<%= StaticAsset(file) %>" as="<%= file.match(/\.css$/) ? 'style' : 'script' %>">
                <% } %>
            <% }) %>
        <% }) %>

        <!-- Include the core styles -->
        <% webpack.chunks.forEach(chunk => { %>
            <% chunk.files.forEach(file => { %>
                <% if (file.match(/\.(css)$/) && chunk.initial) { %>
                    <link rel="stylesheet" href="<%= StaticAsset(file) %>">
                <% } %>
            <% }) %>
        <% }) %>

    </head>
    <body ontouchstart="">

        <!-- No javascript error -->
        <noscript>JavaScript turned off...</noscript>

        <!-- The Vue JS app element -->
        <div id="app"></div>

        <!-- Include the core scripts -->
        <% webpack.chunks.slice().reverse().forEach(chunk => { %>
            <% chunk.files.forEach(file => { %>
                <% if (file.match(/\.(js)$/) && chunk.initial) { %>
                    <script type="text/javascript" src="<%= StaticAsset(file) %>"></script>
                <% } %>
            <% }) %>
        <% }) %>

    </body>
</html>

/service-worker.mix.js(构建服务工作者)

这种混合配置将构建您的 Service Worker ( service-worker.js),并将其放入/dist.

注意:我喜欢在dist每次构建项目时清理我的文件夹,并且由于必须在构建过程的这个阶段运行此功能,因此我将其包含在以下配置中。

const mix   = require('laravel-mix');
const path  = require('path');

// Set the public path
mix.setPublicPath('dist/');

// Define all the javascript files to be compiled
mix.js('src/js/service-worker.js', 'dist');

// Load any plugins required to compile the files
const Dotenv = require('dotenv-webpack');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

// Define the required plugins for webpack
const plugins = [

    // Grant access to the environment variables
    new Dotenv,

    // Ensure the dist folder is cleaned for each build
    new CleanWebpackPlugin

];

// Extend the default Laravel Mix webpack configuration
mix.webpackConfig({
    plugins,
    resolve: {
        alias: {
            '~': path.resolve('')
        }
    }
});

// Disable mix-manifest.json (remove this for Laravel projects)
Mix.manifest.refresh = () => void 0;

/core.mix.js(构建应用程序)

这种混合配置将构建您的主应用程序并将其放置在/dist/js.

这种混合配置有多个关键部分,每个部分都已在其中的评论中明确概述。这些是顶级区域:

  • 代码拆分为app.jsmanifest.jsvendor.js(以及动态导入)
  • Laravel Mix 版本控制不能按 HTML 模板的需要工作,因此laravel-mix-versionhash改为使用
  • html-webpack-plugin用于index.html基于index.ejs模板生成(见上)
  • webpack-pwa-manifest用于生成基于清单的
  • copy-webpack-plugin用于将静态文件复制到/dist目录,并将任何必要的图标复制到站点根目录
  • imagemin-webpack-plugin用于压缩任何静态图像production
  • workbox-webpack-plugin用于将 webpack 清单注入到 service worker 中使用的预缓存数组中。InjectManifest被使用,而不是GenerateSW
  • 构建过程完成后,将应用任何必要的清单转换

上面的内容可能会有所补充,但几乎所有内容都由以下代码中的注释描述:

const config    = require('./config'); // This is where I store project based configurations
const mix       = require('laravel-mix');
const path      = require('path');
const fs        = require('fs');

// Include any laravel mix plugins
// NOTE: not needed in Laravel projects
require('laravel-mix-versionhash');

// Set the public path
mix.setPublicPath('dist/');

// Define all the SASS files to be compiled
mix.sass('src/sass/app.scss', 'dist/css');

// Define all the javascript files to be compiled
mix.js('src/js/app.js', 'dist/js');

// Split the js into bundles
mix.extract([
    // Define the libraries to extract to `vendor`
    // e.g. 'vue'
]);

// Ensure the files are versioned when running in production
// NOTE: This is not needed in Laravel projects, you simply need
// run `mix.version`
if (mix.inProduction()) {
    mix.versionHash({
        length: 8
    });
}

// Set any necessary mix options
mix.options({

    // This doesn't do anything yet, but once the new version
    // of Laravel Mix is released, this 'should' extract the
    // styles from the Vue components and place them in a
    // css file, as opposed to placing them inline
    //extractVueStyles: true,

    // Ensure the urls are not processed
    processCssUrls: false,

    // Apply any postcss plugins
    postCss: [
        require('css-declaration-sorter'),
        require('autoprefixer')
    ]

});

// Disable mix-manifest.json
// NOTE: not needed in Laravel projects
Mix.manifest.refresh = () => void 0;

// Load any plugins required to compile the files
const Dotenv                    = require('dotenv-webpack');
const HtmlWebpackPlugin         = require('html-webpack-plugin');
const WebpackPwaManifest        = require('webpack-pwa-manifest');
const { InjectManifest }        = require('workbox-webpack-plugin');
const CopyWebpackPlugin         = require('copy-webpack-plugin');
const ImageminPlugin            = require('imagemin-webpack-plugin').default;

// Define the required plugins for webpack
const plugins = [

    // Grant access to the environment variables
    new Dotenv,

    // Process and build the html template
    // NOTE: not needed if using Laravel and blade
    new HtmlWebpackPlugin({
        template: path.resolve(__dirname, 'static', 'index.ejs'),
        inject: false,
        minify: !mix.inProduction() ? false : {
            collapseWhitespace: true,
            removeComments: true,
            removeRedundantAttributes: true,
            useShortDoctype: true
        },
        templateParameters: compilation => ({
            webpack: compilation.getStats().toJson(),
            config,
            StaticAsset: (file) => {
                // This will ensure there are no double slashes (bug in Laravel Mix)
                return (config.app.static_url + '/' + file).replace(/([^:]\/)\/+/g, "$1");
            }
        })
    }),

    // Generate the manifest file
    new WebpackPwaManifest({
        publicPath: '',
        filename: 'manifest.json',
        name: config.app.name,
        description: config.meta.description,
        theme_color: config.meta.theme,
        background_color: config.meta.theme,
        orientation: config.app.orientation,
        display: "fullscreen",
        start_url: '/',
        inject: false,
        fingerprints: false,
        related_applications: [
            {
                platform: 'play',
                url: config.app.stores.google.url,
                id: config.app.stores.google.id
            },
            {
                platform: 'itunes',
                url: config.app.stores.apple.url,
                id: config.app.stores.apple.id
            }
        ],
        // TODO: Update this once the application is live
        screenshots: [
            {
                src: config.app.static_url + '/assets/images/misc/screenshot-1-720x1280.png',
                sizes: '1280x720',
                type: 'image/png'
            }
        ],
        icons: [
            {
                src: path.resolve(__dirname, 'static/assets/images/icons/android-chrome-512x512.png'),
                sizes: [72, 96, 128, 144, 152, 192, 384, 512],
                destination: path.join('assets', 'images', 'icons')
            }
        ]
    }),

    // Copy any necessary directories/files
    new CopyWebpackPlugin([
        {
            from: path.resolve(__dirname, 'static'),
            to: path.resolve(__dirname, 'dist'),
            toType: 'dir',
            ignore: ['*.ejs']
        },
        {
            from: path.resolve(__dirname, 'static/assets/images/icons'),
            to: path.resolve(__dirname, 'dist'),
            toType: 'dir'
        }
    ]),

    // Ensure any images are optimised when copied
    new ImageminPlugin({
        disable: process.env.NODE_ENV !== 'production',
        test: /\.(jpe?g|png|gif|svg)$/i
    }),

    new InjectManifest({
        swSrc: path.resolve('dist/service-worker.js'),
        importWorkboxFrom: 'disabled',
        importsDirectory: 'js'
    })

];

// Extend the default Laravel Mix webpack configuration
mix.webpackConfig({
    plugins,
    output: {
        chunkFilename: 'js/[name].js',
    }
}).then(() => {

    // As the precached filename is hashed, we need to read the
    // directory in order to find the filename. Assuming there
    // are no other files called `precache-manifest`, we can assume
    // it is the first value in the filtered array. There is no
    // need to test if [0] has a value because if it doesn't
    // this needs to throw an error
    let filename = fs
        .readdirSync(path.normalize(`${__dirname}/dist/js`))
        .filter(filename => filename.startsWith('precache-manifest'))[0];

    // In order to load the precache manifest file, we need to define
    // self in the global as it is not available in node.
    global['self'] = {};
    require('./dist/js/' + filename);

    let manifest = self.__precacheManifest;

    // Loop through the precache manifest and apply any transformations
    manifest.map(entry => {

        // Remove any double slashes
        entry.url = entry.url.replace(/(\/)\/+/g, "$1");

        // If the filename is hashed then remove the revision
        if (entry.url.match(/\.[0-9a-f]{8}\./)) {
            delete entry.revision;
        }

        // Apply any other transformations or additions here...

    });

    // Filter out any entries that should not be in the manifest
    manifest = manifest.filter(entry => {

        return entry.url.match(/.*\.(css|js|html|json)$/)
            || entry.url.match(/^\/([^\/]+\.(png|ico|svg))$/)
            || entry.url.match(/\/images\/icons\/icon_([^\/]+\.(png))$/)
            || entry.url.match(/\/images\/misc\/splash-([^\/]+\.(png))$/);

    });

    // Concatenate the contents of the precache manifest and then save
    // the file
    const content = 'self.__precacheManifest = (self.__precacheManifest || []).concat(' + JSON.stringify(manifest) + ');';
    fs.writeFileSync('./dist/js/' + filename, content, 'utf8', () => {});

});

/src/js/app.js(主要应用)

这是您注册服务工作者的地方,并且显然定义了您的应用程序等......

/**
 * Register the service worker as soon as the page has finished loading.
 */
if ('serviceWorker' in navigator) {
    window.addEventListener('load', function() {
        // TODO: Broadcast updates of the service worker here...
        navigator.serviceWorker.register('/service-worker.js');
    });
}

// Define the rest of your application here...
// e.g. window.Vue = require('vue');
于 2019-09-03T16:22:21.957 回答