Laravel Mix和WorkBox如何使用?

How to use Laravel Mix and WorkBox?

我正在尝试为我的应用构建 PWA;并花了将近 48 小时试图弄清楚如何将 Workbox 与 Laravel Mix 结合使用。讽刺的是 Google 说 Workbox 是为了让事情变得简单!

呸!

好的,到目前为止我已经弄明白了 -

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

  2. 我不知道如何指定 swSrcswDest 的路径。

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

有人可以帮忙吗?

PS:我几乎阅读了每一篇博客和帮助文章;但 none 谈论可靠地使用 Workbox 与 Laravel 混合。非常感谢您的帮助。

我最近对此做了很多研究,虽然这可能不是您问题的 完整 答案,但它应该给您或访问此页面的任何其他人,足够的入门指南...

我会在学习和研究更多内容时添加到这个答案中。

为了这个答案的目的,我假设你的服务人员被称为 service-worker.js,但是,你显然可以随意称呼它。

步骤 1 - Laravel 混合

假设您在 Laravel Mix 4 中使用 Dynamic Importing in your project (if you aren't, you should be), you will need to downgrade Laravel Mix to version 3. There is an acknowledged bug 阻止 CSS 正确捆绑,并且在发布 Webpack 5 之前不会修复此问题。

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

第 2 步 - 导入或 ImportScripts

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

文档指出:

When using a JavaScript bundler, you don't need (and actually shouldn't use) the workbox global or the workbox-sw module, as you can import the individual package files directly.

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

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

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

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

第 3 步 - 文件结构

假设您使用的是 InjectManifest 而不是 GenerateSW,您将需要编写自己的 service worker,它将在每次构建时通过 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 将只在指定环境中构建 service worker
  • npm run <environment>-core 将只在指定环境中构建核心应用程序

已知问题

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

降级到 Laravel 混合 3

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

这个也可以通过运行ning 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 混合版本控制无法根据需要为 HTML 模板工作,因此使用 laravel-mix-versionhash 代替
  • html-webpack-plugin用于根据index.ejs模板生成index.html(见上文)
  • 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, "");
            }
        })
    }),

    // 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, "");

        // 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');