如何使用 Laravel Mix 和 WorkBox?

The*_*igK 10 laravel progressive-web-apps laravel-mix workbox workbox-webpack-plugin

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

呸!

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

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

  2. 我不知道如何specifiy这些路径swSrcswDest

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

有人可以帮忙吗?

PS:我几乎阅读了所有博客和帮助文章;但没有人谈论可靠地将 Workbox 与 Laravel 混合使用。真的很感激这里的一些帮助。

Ben*_*rey 10

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

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

出于此答案的目的,我假设您的 service worker 被称为service-worker.js,但是,您显然可以随心所欲地称呼它。

第 1 步 - Laravel 混合

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

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

第 2 步 - 导入或导入脚本

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

该文件指出:

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

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

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

  • 我们不希望service-worker.js包含在构建清单中,因为这将被注入到预缓存清单中
  • 我们不想service-worker.js被版本化,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');
    });
}
Run Code Online (Sandbox Code Playgroud)

第 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,建议使用 utliliseimport以确保仅包含必要的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');
Run Code Online (Sandbox Code Playgroud)

/package.json

拆分 Mix 配置

"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"  
}
Run Code Online (Sandbox Code Playgroud)

命令说明

  • 所有标准命令都将像往常一样工作(即npm run dev等)。查看已知问题npm run watch
  • npm run <environment>-service-worker 将在指定的环境中只构建 service worker
  • npm run <environment>-core 将只在指定的环境中构建核心应用程序

已知的问题

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

降级到 Laravel Mix 3

"devDependencies": {  
    "laravel-mix": "^3.0.0"  
}
Run Code Online (Sandbox Code Playgroud)

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

/static/index.ejs

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

<!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>
Run Code Online (Sandbox Code Playgroud)

/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;
Run Code Online (Sandbox Code Playgroud)

/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,