起因

最近在做一个手机站点,服务器用的Google Cloud,但是用户多是国内的。所以在提升页面加载和响应速度这块,是很重视的。

一开始是想使用manifest.json来做Application Cache, 不过后面发现Google和FireFox都有提到打算废弃这个标准并建议大家改用Service Worker。

可是我要做到站点是文章居多的偏新闻性质的,而且交互也不多,用Service Worker来做PWA,感觉有点大材小用。所以想找一些更好实践,而且对于css和js资源的缓存和快速加载有意义的方案。

prefetch和preload就出现在眼前了。

关于prefetch和preload

具体的技术介绍,这里就不详述,大家可以看看MDN的这篇文章

这里说一下preload和prefetch的区别:

preload是通知浏览器尽快的去加载当前页面现在或者将来需要的一些资源文件(css,js,image,viedo,audio,fonts等等),这个加载跟页面解析是并行的,不会阻塞页面本身的加载。

prefetch是一种预测,预测访问当前页面A的用户很可能会访问页面B,所以在页面A提前告知浏览器去加载页面B的资源。这个提前加载的优先级最低,不会干扰页面的正常资源的加载解析,也不会干扰preload。

兼容性

preload在pc上,chrome和firefox支持比较好, eage是还在开发状态,safari的最新版本也已经支持了。在手机端的支持就不是很好,Andriod 需要5之后的webview才支持,国内的UC,QQ和Baidu都不支持;iOS是最新的11.3开始支持

具体参看:caniuse preload

prefetch的兼容性,相对好点,除了MacOS和iOS上的safari不支持之外,其他浏览器都支持了。

具体参看:caniuse prefetch

另外,preload和prefetch都是申明性质的,所以就算不支持,也不会影响现有页面的任何功能

关于站点的webpack配置

为了方便js和css资源的缓存,在nginx上是配置了很长的过期时间,所以对应的,css和js的编译后的文件名都做了hash后缀。这样,线上的更新,都是文件的替换而不是刷新缓存。

为了方便的实现html里面css和js资源文件能在编译后自动更新路径,使用了HtmlWebpackPlugin来实现html的编译和输出

而在HtmlWebpackPlugin本身也有插件机制,其中一个由GoogleChromeLabs开发的插件PreloadWebpackPlugin,就能方便的给编译的html文件添加相关资源的preload和pretch

具体实现(针对多页面多Entry情况)

如果你是一个单页面应用,那么一切都很简单,就不多说了。这里主要说一下在处理多页面的输出时的注意点

首先,说一下我的几个module的版本:

"webpack": "^4.10.2",
"html-webpack-plugin": "^3.2.0",
"preload-webpack-plugin": "^3.0.0-alpha.3",

PreloadWebpackPlugin,我一开始用的是正式版的v2.3.0,可是我的webpack是4.x版本,有点不兼容。按照官方issues上的解决方案,改为安装了preload-webpack-plugin@latest,然后才可以的。

HtmlWebpackPlugin多页面配置

首先,你需要配置好HtmlWebpackPlugin,不同的页面使用不同的template,编译到不同的filename,同时还要指定哪些chunks需要加载到页面里面。

我的站点是目前包括四个页面index.html, book.html, content.html和search.html,对应配置如下:

    new HtmlWebpackPlugin({
      title: 'index',
      template: './src/pages/index/index.html',
      filename: 'index.html',
      chunks: ['index']
    }),
    new HtmlWebpackPlugin({
      title: 'book',
      template: './src/pages/book/book.html',
      filename: 'book.html',
      chunks: ['book']
    }),
    new HtmlWebpackPlugin({
      title: 'content',
      template: './src/pages/content/content.html',
      filename: 'content.html',
      chunks: ['content']
    }),
    new HtmlWebpackPlugin({
      title: 'search',
      template: './src/pages/search/search.html',
      filename: 'search.html',
      chunks: ['search']
    }),

完成这一步之后,编译,dist目录应该有对应的四个文件,里面的资源也能正确加载。接下来就是配置preload了

preload配置

PreloadWebpackPlugin这个插件更适用于单页面应用,对于多页面应用,在配置上比较繁琐一点。

首先,需要使用excludeHtmlNames来反向排除所有页面,只关联你要配置的那个页面,

再然后,使用include选项来明确指定哪些资源要被preload

最后,每一个页面对应添加一个PreloadWebpackPlugin的实例

还有一点需要补充说明一下,在webpack配置的plugins里面,PreloadWebpackPlugin的配置需要在HtmlWebpackPlugin之后,不然PreloadWebpackPlugin找不到HtmlWebpackPlugin提供的hooks,会报错。

完成上面这一步之后的配置如下:

    new HtmlWebpackPlugin({
      title: 'index',
      template: './src/pages/index/index.html',
      filename: 'index.html',
      chunks: ['index']
    }),
    new HtmlWebpackPlugin({
      title: 'book',
      template: './src/pages/book/book.html',
      filename: 'book.html',
      chunks: ['book']
    }),
    new HtmlWebpackPlugin({
      title: 'content',
      template: './src/pages/content/content.html',
      filename: 'content.html',
      chunks: ['content']
    }),
    new HtmlWebpackPlugin({
      title: 'search',
      template: './src/pages/search/search.html',
      filename: 'search.html',
      chunks: ['search']
    }),
    new PreloadWebpackPlugin({
      rel: 'preload',
      excludeHtmlNames: ['book.html', 'content.html', 'search.html'],
      include: ['index']
    }),
    new PreloadWebpackPlugin({
      rel: 'preload',
      excludeHtmlNames: ['index.html', 'content.html', 'search.html'],
      include: ['book']
    }),
    new PreloadWebpackPlugin({
      rel: 'preload',
      excludeHtmlNames: ['index.html', 'book.html', 'search.html'],
      include: ['content']
    }),
    new PreloadWebpackPlugin({
      rel: 'preload',
      excludeHtmlNames: ['index.html', 'book.html', 'content.html'],
      include: ['search']
    }),

可以看到,在excludeHtmlNames的配置上比较繁琐,如果页面多的话,最好写个函数来实现过滤。

prefetch的配置

一个PreloadWebpackPlugin实例同时只能干一件事情,根据rel字段区分,要么是preload,要么是prefetch。所以,还得新增PreloadWebpackPlugin的实例。

不过不着急,先想好如何配置prefetch。在我的这个站点里面,在index页面,大部分的链接都是指向的book页面,所以可以在index页面prefetch book页面的资源。同样的还有,在book页面prefetch content页面的资源。

定好之后,就是配置了,跟preload的配置比较类似,需要区分的就是rel和include这两个选项

rel需要改成prefetch,这不用多说。include,这个时候需要改成你打算prefetch的页面的资源,而不是当前页面的资源。

最终的配置如下:

    new HtmlWebpackPlugin({
      title: 'index',
      template: './src/pages/index/index.html',
      filename: 'index.html',
      chunks: ['index']
    }),
    new HtmlWebpackPlugin({
      title: 'book',
      template: './src/pages/book/book.html',
      filename: 'book.html',
      chunks: ['book']
    }),
    new HtmlWebpackPlugin({
      title: 'content',
      template: './src/pages/content/content.html',
      filename: 'content.html',
      chunks: ['content']
    }),
    new HtmlWebpackPlugin({
      title: 'search',
      template: './src/pages/search/search.html',
      filename: 'search.html',
      chunks: ['search']
    }),
    new PreloadWebpackPlugin({
      rel: 'preload',
      excludeHtmlNames: ['book.html', 'content.html', 'search.html'],
      include: ['index']
    }),
    new PreloadWebpackPlugin({
      rel: 'prefetch',
      excludeHtmlNames: ['book.html', 'content.html', 'search.html'],
      include: ['book']
    }),
    new PreloadWebpackPlugin({
      rel: 'preload',
      excludeHtmlNames: ['index.html', 'content.html', 'search.html'],
      include: ['book']
    }),
    new PreloadWebpackPlugin({
      rel: 'prefetch',
      excludeHtmlNames: ['index.html', 'content.html', 'search.html'],
      include: ['content']
    }),
    new PreloadWebpackPlugin({
      rel: 'preload',
      excludeHtmlNames: ['index.html', 'book.html', 'search.html'],
      include: ['content']
    }),
    new PreloadWebpackPlugin({
      rel: 'preload',
      excludeHtmlNames: ['index.html', 'book.html', 'content.html'],
      include: ['search']
    }),

以上,就能顺利的实现,在webpack编译过程中,在html页面里面自动配置上preload和prefetch。希望能帮到大家

起因

今天尝试使用webpck的import()来做代码分割。

代码类似如下:

import('./nice-scroll').then(init => init(dom))

结果报错:

ERROR in ./js/utils/auto-set-height.js
Module build failed: SyntaxError: ‘import’ and ‘export’ may only appear at the top level (20:8)

分析

在package.json里面确认了一下,我用的webpack版本是^3.5.4,这个是一定支持import()方法的,那么问题应该就出在babel上了。

先截取我的babel-loader的部分配置:

use: {
    loader: 'babel-loader',
    options: {
        // 不采用.babelrc的配置
        "babelrc": false,
        "presets": [
            ["react"],
            ["es2015"]
        ],
        "plugins": [
            "transform-object-rest-spread",
            "transform-class-properties"
        ]
    }
}

一开始我的猜想是plugin es2015里面的transform-es2015-modules-commonjs先于webpack处理了代码,所以报错。

找了一下禁用的方法,改配置如下:

use: {
    loader: 'babel-loader',
    options: {
        // 不采用.babelrc的配置
        "babelrc": false,
        "presets": [
            ["react"],
            ["es2015", {module: false}]
        ],
        "plugins": [
            "transform-object-rest-spread",
            "transform-class-properties"
        ]
    }
}

还是不行。

后面一直各种关键词的搜索,偶然在github上面找到这个问题Dynamic `import()` crashing in `ModuleConcatenationPlugin`的一个回答:

Nope; babel will always process the code before webpack ever sees it; unless for some reason the file itself is being excluded from being processed by babel-loader.
This error is unrelated to babel; it’s a webpack issue.

这个回答,里面说到在webpack面对的代码都是babel处理过的,这个让我坚信问题还是在babel这块,继续搜索。

意外找到这个这篇文章:代码分离 – 使用import()。里面说到了一个插件:syntax-dynamic-import

Babel官方关于这个插件的描述是:

Syntax only
This plugin only allows Babel to parse this syntax. If you want to transform it then see dynamic-import-webpack or dynamic-import-node.

显然这是一个语法解析的插件,使得babel能够理解dynamic import的语法。再联系上面的报错信息里面的SyntaxError,结果有点明显了。

解决

npm install --save-dev babel-plugin-syntax-dynamic-import

然后调整babel-loader配置如下:

use: {
    loader: 'babel-loader',
    options: {
        // 不采用.babelrc的配置
        "babelrc": false,
        "presets": [
            ["react"],
            ["es2015", { "modules": false }]
        ],
        "plugins": [
            "syntax-dynamic-import",
            "transform-object-rest-spread",
            "transform-class-properties"
        ]
    }
}

重启webpack,顺利通过编译!!!

从一个Electron项目说起

这两天在折腾用Electron来写一个弹幕助手,方便用PC直播的主播能够看到用户发的弹幕并且进行回复和相关管理操作。

主要的开发任务就是对PC站点已有功能的搬迁和调整。

在最后实现退出功能的时候,希望是在菜单栏上有一个退出按钮,用户点击就能直接退出。

下面是mainProcess里面配置菜单的代码,针对退出登录按钮注册了事件,点击的时候,通知对应的web页去实现退出登录功能。

const template = [
    {
        label: '操作',
        submenu: [
            {
                label: '关闭助手',
                role: 'quit'
            },
            {
                label: '退出登录',
                click: function(){
                    mainWindow.webContents.send('main-process-message', '{action: "quit-login"}');
                }
            }
        ]
    }
];

然后是renderProcess这边监听事件并对应处理

const ipc = require('electron').ipcRenderer;
ipc.on('main-process-message', (event, data) => {
    switch(data.action) {
        case "quit-login": 
            user.quit();
            console.log('quit');
            break;
    }
});

一个意外的错误

项目这块,renderProcess的代码都是用webpack进行编译的。

在renderProcess里面引入electron模块之后,立马就报了一个问题:

ERROR in ./~/.1.6.2@electron/index.js
Module not found: Error: Can't resolve 'fs' in '/xxx/electron-quick-start/node_modules/.1.6.2@electron'
@ ./~/.1.6.2@electron/index.js 1:9-22
@ ./js/index.js

上网看了一圈,发现github上的webpack项目里面有人遇到了类似的问题:

Error: Cannot resolve module ‘fs’ with electron and js

然后底下有人给出了解决方式,在webpack.config.js里面加上target: 'electron'就可以的。

试了一下,发现果然是可以的。

深入了解target属性

解决问题之后,出于好奇,就去webpack官网看了target配置的文档

webpack可以为js的各种不同的宿主环境提供编译功能,为了能正确的进行编译,就需要开发人员在配置里面正确的进行配置。

默认情况下,target的值是web,也就是为类浏览器的环境提供编译。

完整的target属性值及对应作用的列表如下:

拷贝自https://github.com/webpack/webpack.js.org/edit/master/content/configuration/target.md

target Description
async-node Compile for usage in a Node.js-like environment (uses fs and vm to load chunks asynchronously)
~~atom~~ Alias for electron-main
~~electron~~ Alias for electron-main
electron-main Compile for Electron for main process.
electron-renderer Compile for Electron for renderer process, providing a target using JsonpTemplatePlugin, FunctionModulePlugin for browser environments and NodeTargetPlugin and ExternalsPlugin for CommonJS and Electron built-in modules.
node Compile for usage in a Node.js-like environment (uses Node.js require to load chunks)
node-webkit Compile for usage in WebKit and uses JSONP for chunk loading. Allows importing of built-in Node.js modules and nw.gui (experimental)
web Compile for usage in a browser-like environment (default)
webworker Compile as WebWorker

所以,把target设为electron,也就能正确的对Electron环境下的代码进行编译了,不过我的代码是写在renderProcess里面的,所以把targets设置为electron-render才是更加合理的选择。