Coder Social home page Coder Social logo

blog's People

Contributors

cpselvis avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

blog's Issues

为什么 webpack4 默认支持 ES6 语法的压缩?

在专栏课程里,有位同学提到过一个很有意思的问题:“我没装 babel,js 入口里写了个箭头函数,运行 webpack 构建命令后,也成功编译了。这是为什么?”。今天就带领大家一起去探讨下这个话题。

在使用 webpack 的时候,很常见的一个构建优化手段就是缩小构建目标。比如在构建阶段只构建 src 里面的模块代码,对于 node_modules 里面所引入的三方包不进行构建操作。

发现问题

如果使用的是 webpack 3.x 版本,编写的构建脚本类似这样的,我们通过设置loader 里面的 exclude 字段避免由于解析 node_modules 里面的模块造成的构建耗时:

const path = require('path');
const webpack = require('webpack');

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.join(__dirname, 'dist'),
        filename: 'bundle.js'
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                loader: 'happypack/loader',
                exclude: path.join(__dirname, 'node_modules')
            }
        ]
    }
    plugins: [
        new webpack.optimize.UglifyJsPlugin()
    ]
};

我们经常会遇到一个问题,假设引入的 npm 包质量不够高,比如 node_modules 里面有 ES6 的语法,那么 webpack 在 uglify 阶段会报错!下面给出两种常见的出错场景:

ES6 的模板字符串

假设 node_modules 里面存在 ES6 的模板字符串语法,那么在生产环境打包的代码压缩阶段,UglifyJs 会抛出错误。

图片

ES6 的箭头函数

同样的,你使用 ES6 的箭头函数也是无法正常的压缩代码的。

图片

**细心的你一定会发现如果使用的是 webpack 4,这个场景描述的问题将不再出现。**webpack 4默认支持 ES6 代码的压缩,这个是什么原因呢?

初步分析

如果你有对 webpack 4 的依赖包进行过相关分析,比如直接查阅 package.json 文件或者通过 http://npm.broofa.com/ 网站上进行 webpack 依赖图分析。不难发现 webpack 4 里面使用了 terser-webpack-plugin 插件替代了之前一直使用的 uglifyjs-webpack-plugin 作为它的内置插件。

以 4.39.3 这个版本为例,可以看到它的 package.json 文件的依赖包括了terser-webpack-plugin。

图片

我们进一步分析发现 webpack 的 4.26.0 这个版本有一次提交,它的提交内容是对 webpack 内置插件进行了一次切换。

图片

经过这么一次分析,我们可以知道 webpack 4 之所以具备默认压缩 ES6 代码的能力,离不开 **terser-webpack-plugin **所起的作用!

进一步分析

在探究 terser-webpack-plugin 插件的原理前,我们先系统的回顾一下代码压缩插件的历史:

  • 当 uglifyjs-webpack-plugin 版本小于 v1.0 时,它使用的是 uglify-js 依赖
  • 但是 uglify-js 并不支持 ES6, 因此在 uglify-js 仓库的 harmony 分支 Fork 了一个 uglify-es
  • uglifyjs-webpack-plugin 的 v1.x 为了支持 ES6 的压缩语法,将 uglify-js 依赖切换到了 uglify-es
  • 但是 uglify-es 停止维护了: mishoo/UglifyJS#3156 (comment)
  • uglify-es 的停止维护导致了 terser 被 fork 出来了,并且 terser 处理了没有合入的 PRs,最终创建了一个独立的仓库: https://github.com/fabiosantoscode/terser
  • 随后,terser-webpack-plugin 被创建出来, 它基于 terser,并且具备uglifyjs-webpack-plugin 的同等功能 : https://github.com/webpack-contrib/terser-webpack-plugin
  • 由于 uglifyjs-webpack-plugin v2.x 回退到了 uglify-js, 不再支持 ES6。 因此那些希望支持 ES6 语法压缩的项目必须切换到 terser-webpack-plugin

备注:压缩插件历史的来源 webpack/webpack@311a728

到这里,我们可以得出一个基本的结论:terser-webpack-plugin 基于 terser 因此它具备 ES6 的压缩能力,uglifyjs-webpack-plugin v2.x 版本基于 uglify-js,无法支持 ES6 的压缩。

插件 依赖 是否支持 ES6(Y/N)
terser-webpack-plugin terser Y
uglifyjs-webpack-plugin v1.x uglify-es Y
uglifyjs-webpack-plugin v2.x uglify-js N

原理探究

代码压缩原理其实挺简单的,也是 AST 的一个经典的应用案例。它的压缩过程通常是:

     JS 源代码 -> AST -> 美化、压缩 -> 新的 AST -> 压缩后的代码 

了解了代码压缩的基本流程后,接下来我们看看源码包含了哪些内容,由于 terser 是从 uglify-es Fork 出来进行修改的,因此它的代码结构和 uglify-js 基本一致,只不过 terser 使用了 ES6 模块的静态分析功能。我们以 terser 的源码为例分析下:

  • ast.js:JS 的抽象语法树的描述信息
  • parse.js:Parser,用于从 JS 源代码分析出 AST
  • minify.js:用于将 AST 优化成更简短的结构
  • output.js:代码生成器,从 AST 输出 压缩后的代码,支持 sourcemap 的生成
  • propmangle.js:对变量的长度进行压缩,通常是单个字符
  • scope.js:分析变量定义/引用位置的信息
  • transform.js:节点遍历

然后,我们来一探 terser 和 uglify-js 的差异。对比了之后,发现一个很大的差异是 AST 的支持上面不同。

分析AST的差异发现,下面是两个文件 diff 对比只在 terser 中才有,而这些刚好对应 ES6 的语法。

AST_Arrow,
AST_Await,
AST_BigInt,
AST_Class,
AST_ClassExpression,
AST_ConciseMethod,
AST_Const,
AST_DefaultAssign,
AST_Destructuring,
AST_Expansion,
AST_Export,
AST_ForOf,
AST_Import,
AST_Let,
AST_NameMapping,
AST_NewTarget,
AST_PrefixedTemplateString,
AST_Super,
AST_SymbolMethod,
AST_TemplateSegment,
AST_TemplateString,
AST_Yield

至此,我们发现 webpack4 默认支持 ES6 压缩的关键是:terser 里面实现了 ES6 语法的 AST解析

学透 Electron 自定义菜单

导语:近几年,随着 Electron/ NW.js 等技术的兴起,也催生了一批优秀的桌面端开发者工具,比如 VSCode、微信开发者工具、飞冰(ICE) 等等。对于开发者而言,桌面端开发者工具的优势是:可视化能力、操作系统层面的 API 访问、和良好的开发调试体验。因此,最近准备系统性的深入学习下 Electron 技术并且将学习的知识进行适当沉淀。本篇文章主要总结 Electron 的自定义菜单。

传统的 Web APP 的开发基本上不会涉及到菜单,但是在 Electron 里面它提供了对于菜单全面的控制,你可以通过 Menu、MenuItem 模块来创建应用所需的自定义菜单。这篇文章我们一起探讨下 Electron 中有哪些菜单种类,又是如何通过代码去自定义菜单的?

首先,我们一起看看基本的菜单介绍,方便大家对于基本的概念有初步的认识。

菜单介绍

Electron 里的菜单大体上分为三类:应用菜单、上下文菜单和 Dock 菜单(仅针对 OSX 系统)。

这里以微信开发者工具为例(微信开发者工具基于 NW.js 进行开发,主要出于 Windows XP兼容性考虑),来分别介绍这几种菜单的含义。打开微信开发者工具,可以通过下图,很清晰的发现3个菜单所处的位置。

这三种菜单的含义分别是:

  • 应用菜单:应用菜单通常位于应用程序的顶部,提供了用户可能用到的各种操作,如程序的快捷方式、常用的文件夹及系统命令等。
  • 上下文菜单:在应用里面点击右键看到的菜单。
  • Dock 菜单:只在 OSX 系统才有,通常功能较少,提供特别常用的功能。

了解了菜单的基本概念后,接下来我们一起看看如何通过代码去实现自定义菜单的功能。

应用菜单

首先看看应用菜单,Electron 默认会有一个标准的应用菜单,我们一起看看默认的应用菜单效果:

仔细分析下默认应用菜单包括的菜单结构如下:

如果你希望定制应用菜单,你需要自行实现整个菜单的定义。这里需要注意,应用菜单只能在 Electron 的主进程中进行访问。例如:

// main.js
const {
    app,
    Menu
 } = require('electron');

app.on('ready', () => {
    const appMenu = Menu.buildFromTemplate(menuTemplate);
    Menu.setApplicationMenu(appMenu);
});

这里面重点关注 app 的 ready 这段代码块,应用菜单通过 **Menu.setApplicationMenu **进行设置。接下来分别从菜单模板、分隔符、快捷键和子菜单几个方面来系统介绍下应用菜单的内容。

菜单模板:

菜单的 template 是一个对象数组,每个对象会定义一个独立的菜单,它会显示在应用菜单的 Bar 位置,显示的文字通过 label 属性进行定义。

以这段代码为例,我们定义了两个菜单,每个菜单都包含两个菜单项,菜单项就是我们点击菜单时下拉出来的内容。

const template = [
    {
        label: 'Edit App',
        submenu: [
            {
                label: 'Undo'
            },
            {
                label: 'Redo'
            }
        ]
    },
    {
        label: 'View App',
        submenu: [
            {
                label: 'Reload'
            },
            {
                label: 'Toggle Full Screen'
            }
        ]
    }
];

对应的效果:

这里值得注意的是:对于 OSX 而言,应用菜单的第一个菜单项是应用程序的名字,会使得 Edit App 这个菜单被覆盖掉。因此,我们需要针对 OSX 进行特殊处理,处理的过程通常是:

if (process.platform === 'darwin') {
    template.unshift({
        label: app.getName(),
        submenu: [
            {
                label: 'Quit',
                accelerator: 'CmdOrCtrl+Q',
                click() {
                    app.quit();
                }
            }
        ]
    });
}

分隔符:

通过 type: 'separator' 可以在两个菜单项之间定义一个分隔符,分隔符的作用主要是将功能相似的菜单项分隔在一起,便于更好的操作。

const template = [
    {
        label: 'Edit App',
        submenu: [
            {
                label: 'Undo'
            },
            {
                type: 'separator'
            },
            {
                label: 'Redo'
            }
        ]
    }
];

可以看到,Undo 和 Redo 之间出现了一个分隔符。

接下来,我们一起了解下常用的快捷键和内置的 role 功能。

快捷键:

快捷键我们日常开发过程中用得很多,比如 Ctrl + A 全选,Ctrl + C 复制,Ctrl + V 粘贴。可以供我们选择的快捷键有:

  • Command (简写Cmd)
  • Control(简写Ctrl)
  • CommandOrControl(简写CmdOrCtrl)
  • Alt
  • Option
  • AltGr
  • Shift
  • Super

我们把上面的代码修改一下,增加快捷键,快捷键通过 accelerator 属性进行定义。

const template = [
    {
        label: 'Edit App',
        submenu: [
            {
                label: 'Undo',
                accelerator: 'CmdOrCtrl+Z'
            },
            {
                type: 'separator'
            },
            {
                label: 'Redo',
                accelerator: 'Shift+CmdOrCtrl+Z',
            }
        ]
    }
];

添加完快捷键后,可能你会问,点击某个菜单或者某个快捷键后如何触发相应的逻辑呢?这个可以通过编写 click() 自定义回调函数或者使用 Electron 内置的 role 进行指定。我们将上述代码继续修改:

const template = [
    {
        label: 'Edit App',
        submenu: [
            {
                label: 'Undo',
                accelerator: 'CmdOrCtrl+Z',
                role: 'undo'
            },
            {
                type: 'separator'
            },
            {
                label: 'Redo',
                accelerator: 'Shift+CmdOrCtrl+Z',
                role: 'redo'
            }
        ]
    }
];

增加了 role 之后可以发现就有对应的操作效果了,Electron 的所有内置的 role 如下:

  • undo: 撤销
  • redo:重做
  • cut:剪切
  • copy:复制
  • paste:粘贴
  • pasteAndMatchStyle
  • selectAll:全选
  • delete:删除
  • minimize:当前窗口最小化
  • close:关闭当前窗口
  • quit:退出应用程序
  • reload:刷新当前窗口
  • forceReload:强制刷新当前窗口,忽略缓存
  • toggleDevTools:打开或者关闭 devtool
  • togglefullscreen:进行全屏切换
  • resetZoom:重置窗口大小
  • zoomIn:放大窗口的10%.
  • zoomOut:缩小窗口的10%.

完整的 Role 可以查看:https://electronjs.org/docs/api/menu-item#roles

子菜单:

我们在前面的基础上增加一个新的菜单 Sub Menu,可以看到这个菜单里面的菜单项新增了 submenu 属性,通过这个属性可以继续定义子菜单,此处我们定义了 Submenu item1 和 Submenu item2。

const template = [
    {
        label: 'Edit App',
        submenu: [
            {
                label: 'Undo',
                accelerator: 'CmdOrCtrl+Z',
                role: 'undo'
            },
            {
                type: 'separator'
            },
            {
                label: 'Redo',
                accelerator: 'Shift+CmdOrCtrl+Z',
                role: 'redo'
            }
        ]
    },
    {
        label: 'Sub Menu',
        submenu: [
            {
                label: 'Submenu item',
                submenu: [
                    {
                        label: 'Submenu item1'
                    },
                    {
                        label: 'Submenu item2'
                    }
                ]
            }
        ]
    },
];

子菜单的效果如下:

到这里,应用菜单这个最重要的内容就介绍完了,接下来我们看看上下文菜单这个部分。

上下文菜单

上下文菜单(context menu)就是我们通常说的右键菜单,文章开头有展示效果。需要注意的是:上下文菜单,需要在渲染进程中进行实现。在渲染进程中是需要通过remote模块调用主进程中的模块。

实现上下文菜单很简单,只需要监听到 contextmenu 事件,然后将菜单展示出来即可。

//renderer.js
const { remote } = require('electron');
const { Menu } = remote;

const createContextMenu = () => {
    const contextTemplate = [
        {
            label: 'Cut',
            role: 'cut'
        },
        {
            label: 'Copy',
            role: 'copy'
        }
    ];
    const contextMenu = Menu.buildFromTemplate(contextTemplate);
    return contextMenu;
}

window.addEventListener('contextmenu', (event) => {
    event.preventDefault();
    const contextMenu = createContextMenu();
    contextMenu.popup({
        window: remote.getCurrentWindow()
    });
}, false);

Dock菜单

最后,我们一起看看 Dock 菜单,Dock 的菜单实现也是在主进程中,实现思路和前面基本类似,核心是通过 app.dock.setMenu 这个 API 进行实现的。

// main.js
const createDockMenu = () => {
    const dockTempalte = [
        {
            label: 'New Window',
            click () {
                console.log('New Window');
            }
        }, {
            label: 'New Window with Settings',
            submenu: [
                { label: 'Basic' },
                { label: 'Pro' }
            ]
        },
        {
            label: 'New Command...'
        }
    ];

    const dockMenu = Menu.buildFromTemplate(dockTempalte);
    app.dock.setMenu(dockMenu);
}

app.on('ready', function() {
    createDockMenu();
});

Dock 菜单的效果如下:

至此,这篇文章到这里就结束了,感谢您的阅读。后面的文章会涉及到对话框、 IPC 通信、Electron 应用的测试、打包、发布和自动更新等内容。

webpack4 中如何实现资源内联?

在专栏课程里,关于 CSS 内联这部分没有进行演示。今天就再系统的介绍下 Webpack4 里面资源内联(HTML/CSS/JS/Image/Font)的正确姿势吧!

首先,我们一起了解下什么是资源内联。

什么是资源内联?

资源内联(inline resource),就是将一个资源以内联的方式嵌入进另一个资源里面,我们通过几个小例子来直观感受一下。

HTML 内联 CSS,这个其实就是我们通常说的 内联 CSS 或者 行内 CSS。我们可以写几行 reset CSS,然后通过 style 标签的方式嵌入进了 HTML 里面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        * {
            margin: 0;
            padding: 0;
        }
        body {
            font-size: 12px;
            font-family: Arial, Helvetica, sans-serif;
            background: #fff;
        }
        ul, ol, li {
            list-style-type: none;
        }
    </style>
</head>
<body>
    
</body>
</html>

CSS 内联图片,就是我们通常将小图片通过 base64 的方式内嵌进 CSS 里面。我们可以将搜索小 icon 内联进 CSS:

// index.css
.search {
  background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABJ0lEQVQ4T6XSsUoEMRAG4H/ClZaLmbSW1pZ6+gAnFrK+gZXoK6jvIILgE6gIcnYWgmJno6AgYp1Z2EcIGQnsHbuaQ9abMkO+TGaGMGfQnPfxC3DOrajqPoB1AArgnohOvffPucc6ADMfAjgCUMYYH9MFY8wagEsAxyKScp2YAtbaERGNRST7LWZWVd2squq2LbSBMyK6E5GrXKnW2i1jzMh7v5sFmPkzhDCs69rngKIo3GAweBKRpVnAVwhh9Q/gRUQWs4Bz7jzGeFNV1ThXATOXAA5EJDV1Gr2aSETb3vvrLJAOmTmNKY2yVNUHVSVjzBDABYA3ADsi8j4TSIlmkfYAbABYUNUPACdE9NpAHaTXKjPz8k+kF9B8s4P0BibIpBf/AtpN/AYx54AR58WxmQAAAABJRU5ErkJggg==) no-repeat;
}

了解了资源内联的基本概念后,可能你会问资源内联有什么意义?接下来我们从几个维度去看看为什么我们需要资源内联。

资源内联的意义

资源内联的意义这里我从三个方面去说明一下,分别是:工程维护、页面加载性能、页面加载体验。

工程维护

我们看看资源内联对于工程维护的意义,这个是一个基本的 HTML 结构。在如今流行的 Hybrid 混合开发架构里,会有一个个的 H5 页面,对应前端工程里的多页面应用(MPA)。

HTML Structure

我们去打包多页面应用的时候会借助 html-webpack-plugin,每个页面会有一个 HTML 模板与之对应。每个 HTML 模板都会包含很多相似的内容,比如 meta 信息,或 SSR 时需要用到的一些占位符等等。试想一下,如果将下面这段 meta 代码分别复制一份放到每个 HTML 模板里面将会对代码维护造成的影响。

<meta charset="UTF-8">
<meta name="viewport" content="viewport-fit=cover,width=device-width,initial-scale=1,user-scalable=no">
<meta name="format-detection" content="telephone=no">
<meta name="keywords" content="now,now直播,直播,腾讯直播,QQ直播,美女直播,附近直播,才艺直播,小视频,个人直播,美女视频,在线直播,手机直播">
<meta name="name" itemprop="name" content="NOW直播—腾讯旗下全民视频社交直播平台"><meta name="description" itemprop="description" content="NOW直播,腾讯旗下全民高清视频直播平台,汇集中外大咖,最in网红,草根偶像,明星艺人,校花,小鲜肉,逗逼段子手,各类美食、音乐、旅游、时尚、健身达人与你24小时不间断互动直播,各种奇葩刺激的直播玩法,让你跃跃欲试,你会发现,原来人人都可以当主播赚钱!">
<meta name="image" itemprop="image" content="https://pub.idqqimg.com/pc/misc/files/20170831/60b60446e34b40b98fa26afcc62a5f74.jpg"><meta name="baidu-site-verification" content="G4ovcyX25V">
<meta name="apple-mobile-web-app-capable" content="no">
<meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
<link rel="dns-prefetch" href="//11.url.cn/">
<link rel="dns-prefetch" href="//open.mobile.qq.com/">

这个时候推荐的做法是维护一份 meta.html,将上面的这个代码内容放置进去。每个 HTML 模板将 meta.html 片段内联进去。

工程维护的另一个比较常见的场景就是图片、字体等文件的内联了,比如很多同学通常会去网上找一个在线的 base64 编码工具(如:https://www.base64code.com/ )去将各种图片(png、jpg、gif) 或者 字体 (ttf、otf) 编码,然后将编码后的那一长串字符串放置到代码里面去。比如前面的这个搜索 icon 图标,这段长串的字符串放置在源代码里面根本毫无语义,而且对维护者而言也是场灾难。

// index.css
.search {
  background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABJ0lEQVQ4T6XSsUoEMRAG4H/ClZaLmbSW1pZ6+gAnFrK+gZXoK6jvIILgE6gIcnYWgmJno6AgYp1Z2EcIGQnsHbuaQ9abMkO+TGaGMGfQnPfxC3DOrajqPoB1AArgnohOvffPucc6ADMfAjgCUMYYH9MFY8wagEsAxyKScp2YAtbaERGNRST7LWZWVd2squq2LbSBMyK6E5GrXKnW2i1jzMh7v5sFmPkzhDCs69rngKIo3GAweBKRpVnAVwhh9Q/gRUQWs4Bz7jzGeFNV1ThXATOXAA5EJDV1Gr2aSETb3vvrLJAOmTmNKY2yVNUHVSVjzBDABYA3ADsi8j4TSIlmkfYAbABYUNUPACdE9NpAHaTXKjPz8k+kF9B8s4P0BibIpBf/AtpN/AYx54AR58WxmQAAAABJRU5ErkJggg==) no-repeat;
}

我们可以通过更优雅的资源内联语法来避免这个问题,文章后面会介绍到。

页面加载性能

资源内联的第2点意义在于可以减少 HTTP 的请求数,当然如果你的网站有使用 HTTP2 这点的意义可能不会那么大。将各种小图片、小字体(比如:小于5k) 在生产环境 base64 到代码里面可以极大的减少页面的请求数量,从而提升页面的加载时间。

页面加载体验

资源内联另外一个重要的意义在于提升页面加载体验。我们都知道浏览器解析 HTML源码是从上到下解析,因此我们会把 CSS 放到头部,JS 放置到底部。以 SSR 场景为例,如果不将打包出来的 CSS 内联进 HTML 里面,HTML 出来的时候页面的结构已经有了,但是还需要发送一次请求去请求 css,这个时候就会出现页面闪烁,网络情况差的时候更加明显。

资源内联的类型

资源内联的类型主要包含:

  • HTML 内联
  • CSS 内联
  • JS 内联
  • 图片、字体内联

如果你曾经使用过 FIS 或者看过 FIS 的文档,你会发现 FIS 对于资源内联的支持非常棒,详细的文档:嵌入资源

FIS HTML 内联 HTML 片段:

 <link rel="import" href="demo.html?__inline">

FIS HTML 内联 JS 脚本:

  <script type="text/javascript" src="demo.js?__inline"></script>

接下来,我们分别看看每种内联在 webpack4 中的实现。

HTML 内联

基础版

HTML 内联 HTML 片段、CSS 或者 JS(babel 编译后的,比如内联某个 npm 组件) 的思路很简单,就是直接读取某个文件的内容,然后插入到对应的位置。我们可以借助 [email protected]版本,最新的 raw-loader 会有问题(因为它导出模块时是使用 export default),不过你完全可以自己实现这样的一个 raw-loader。

0.5.1 版本的 raw-loader 的代码:

module.exports = function(content) {
	this.cacheable && this.cacheable();
	this.value = content;
	return "module.exports = " + JSON.stringify(content);
}

借助 raw-loader 实现的内联语法如下:

// 内联 HTML 片段
${ require('raw-loader!./meta.html')}

// 内联 JS
<script>${ require('raw-loader!babel-loader!../../node_modules/lib-flexible/flexible.js')}</script>

增强版

我们可以实现一个对开发者更友好的语法糖,比如实现一个 loader 去解析 HTML 里面的?__inline 语法。这里我实现了一个 html-inline-loader,它的代码如下:

const fs = require('fs');
const path = require('path');

const getContent = (matched, reg, resourcePath) => {
    const result = matched.match(reg);
    const relativePath = result && result[1];
    const absolutePath = path.join(path.dirname(resourcePath), relativePath);
    return fs.readFileSync(absolutePath, 'utf-8');
};

module.exports = function(content) {
  const htmlReg = /<link.*?href=".*?\__inline">/gmi;
  const jsReg = /<script.*?src=".*?\?__inline".*?>.*?<\/script>/gmi;

  content = content.replace(jsReg, (matched) => {
    const jsContent = getContent(matched, /src="(.*)\?__inline/, this.resourcePath);
    return `<script type="text/javascript">${jsContent}</script>`;
  }).replace(htmlReg, (matched) => {
    const htmlContent = getContent(matched, /href="(.*)\?__inline/, this.resourcePath);
    return htmlContent;
  });

  return `module.exports = ${JSON.stringify(content)}`;
}

然后,你可以这样使用:

<!DOCTYPE html>
<html lang="en">
<head>
    <link href="./meta.html?__inline">
    <title>Document</title>
    <script type="text/javascript" src="../../node_modules/lib-flexible/flexible.js?__inline"></script>
</head>
<body>
    <div id="root"><!--HTML_PLACEHOLDER--></div>
    <!--INITIAL_DATA_PLACEHOLDER-->
</body>
</html>

查看的效果:

CSS 内联

通常情况下,为了更好的加载体验,我们会将打包好的 CSS 内联到 HTML 头部,这样 HTML 加载完成 CSS 就可以直接渲染出来,避免页面闪动的情况。那么 CSS 内联如何实现呢?

CSS 内联的核心思路是:将页面打包过程的产生的所有 CSS 提取成一个独立的文件,然后将这个 CSS 文件内联进 HTML head 里面。这里需要借助 mini-css-extract-plugin 和 html-inline-css-webpack-plugin 来实现 CSS 的内联功能。

// webpack.config.js

const path = require('path');

module.exports = {
    entry: {
        index: './src/index.js',
        search: './src/search.js'
    },
    output: {
        path: path.join(__dirname, 'dist'),
        filename: '[name]_[chunkhash:8].js'
    },
    mode: 'production',
    plugins: [
        new MiniCssExtractPlugin({
            filename: '[name]_[contenthash:8].css'
        }),
        new HtmlWebpackPlugin(),
        new HTMLInlineCSSWebpackPlugin()
    ]
};

注:html-inline-css-webpack-plugin 需要放在 html-webpack-plugin 后面。

图片、字体内联

基础版

图片和字体的内联可以借助 url-loader,比如你可以通过修改 webpack 配置让小于 10k 的图片或者字体文件在构建阶段自动 base64。

// webpack.config.js

const path = require('path');

module.exports = {
    entry: {
        index: './src/index.js',
        search: './src/search.js'
    },
    output: {
        path: path.join(__dirname, 'dist'),
        filename: '[name]_[chunkhash:8].js'
    },
    mode: 'production',
    module: {
        rules: [
            {
                test: /.(png|jpg|gif|jpeg)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            name: '[name]_[hash:8].[ext]',
                            limit: 10240
                        }
                    }
                ]
            },
            {
                test: /.(woff|woff2|eot|ttf|otf)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            name: '[name]_[hash:8][ext]',
                            limit: 10240
                        }
                    }
                ]
            }
        ]
    }
};

增强版

不过 url-loader 做资源内联最大的缺陷就是 不能个性化的去设置某张图片自动编码,针对这个问题,我们可以借鉴下 FIS 的语法糖,实现 ?__inline 的语法糖,引用某个图片的时候看到这个后缀则自动的将这张图片进行 base64 编码。这个功能实现起来也很简单,可以参考我实现的 inline-file-loader,核心代码:

export default function loader(content) {
  const options = loaderUtils.getOptions(this) || {};

  validateOptions(schema, options, {
    name: 'File Loader',
    baseDataPath: 'options',
  });

  const hasInlineFlag = /\?__inline$/.test(this.resource);

  if (hasInlineFlag) {
    const file = this.resourcePath;
    // Get MIME type
    const mimetype = options.mimetype || mime.getType(file);

    if (typeof content === 'string') {
      content = Buffer.from(content);
    }

    return `module.exports = ${JSON.stringify(
      `data:${mimetype || ''};base64,${content.toString('base64')}`
    )}`;
  }

有了图片的内联功能,我们可以将前面的搜索 icon 图标内联的写法修改成:

// index.css
.search {
  background: url(./search-icon.png?__inline) no-repeat;
}

最后

下面是本篇文章的代码演示资料,如果有需求,可以自行获取。

学透 Electron 自定义 Dock 图标

Mac OS 做为前端开发者的首选操作系统相信大家再熟悉不过了,在电脑主界面的底部可以看到各种各样的应用程序图标。比如:App Store、Safari 浏览器、照片、短信等等。使用 Electron 开发时我们也会发现有一个默认的图标,但总感觉这个图标不够酷。那么,我们要如何自定义 Dock 图标呢?

Dock 介绍

Dock 是 Mac OS 电脑主界面底部的应用程序集合,可以理解成 windows 下的桌面快捷方式。通常,我们会把比较常用的软件锁定在 Dock 下,便于快速找到和使用它们。

1

Dock 图标

Dock 图标在 UI 上总共包括两方面的内容:图标 Logo 和消息条数。

如果不做任何设置,Electron 默认的应用程序图标如图所示:

4

那么,Electron 开发中要如何自定义 Dock 图标呢?

自定义 Dock 图标

首先,我们去苹果开发者官网上去下载一个图标。

9

这里我下载的是 Facetime 这个应用的 Logo,然后我们给 BrowserWindow 这个对象添加一个 icon 属性,然后看看效果。

function createWindow() {
    mainWindow = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            nodeIntegration: true
        },
++      icon: path.join(__dirname, 'assets/images/facetime.png')
    });

    mainWindow.loadFile('index.html');

    mainWindow.on('close', function() {
        mainWindow = null;
    });
}

发现并没有变化?这个其实是正常的,BrowserWindow 对象的 icon 属性只对 windows/Linux 系统生效,对于 Mac OS 需要通过 app.dock.setIcon 进行设置。我们将上面的代码修改如下:

function createWindow() {
    mainWindow = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            nodeIntegration: true
        },
        icon: path.join(__dirname, 'assets/images/facetime.png')
    });
    
++  if (process.platform === 'darwin') {
++      app.dock.setIcon(path.join(__dirname, 'assets/images/facetime.png'));
++  }

    mainWindow.loadFile('index.html');

    mainWindow.on('close', function() {
        mainWindow = null;
    });
}

修改之后,我们再运行下代码发现 icon 已经生效了。

5

设置 Dock 标识

我们经常会发现 Dock 里面的图标右上方会有消息通知(Dock badges),比如 App Store 有多少个已安装的软件可以更新,QQ 上有多少条未读的消息等等。这个 Dock 标识在 Electron 中要如何设置呢?

我们可以通过 app.dock.setBadge API 进行设置。下面我们实现当应用窗口失去焦点时让消息通知的标识加1的功能。

function createWindow() {
    mainWindow = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            nodeIntegration: true
        },
        icon: path.join(__dirname, 'assets/images/facetime.png')
    });
    
    if (process.platform === 'darwin') {
        app.dock.setIcon(path.join(__dirname, 'assets/images/facetime.png'));
    }

    mainWindow.loadFile('index.html');

    mainWindow.on('close', function() {
        mainWindow = null;
    });
    
++  mainWindow.on('blur', () => {
++      const badgeString = app.dock.getBadge();
++      if (badgeString === '') {
++          app.dock.setBadge('1');
++      } else {
++        app.dock.setBadge((parseInt(badgeString) + 1).toString());
++      }
++  });
}

效果如图:
6

Dock 弹跳

系统的了解 Dock 图标的自定义设置和 Dock 标识之后,接下来我们看看 Dock 里另一个比较重要的功能:Dock 弹跳。

Dock 弹跳通常用于重要信息的通知,因为它会比较引人注意。比如:网络断开的时候,QQ 会弹跳一次。接下来我们看看如何使用这个功能的?

Dock 弹跳是通过 app.dock.bounce() 这个 API 进行实现的,它的参数可以是 information 或者 critical,默认值是 information。两个参数的区别是:information 用于消息的通知,它仅仅会使图标弹跳一次,而 critical 会使得图标一直弹跳直到应用处于激活状态或者手动取消弹跳。

下面,我们实现窗口启动5秒后触发弹跳功能:

function createWindow() {
    mainWindow = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            nodeIntegration: true
        },
        icon: path.join(__dirname, 'assets/images/facetime.png')
    });
    
    if (process.platform === 'darwin') {
        app.dock.setIcon(path.join(__dirname, 'assets/images/facetime.png'));
    }

    mainWindow.loadFile('index.html');

    mainWindow.on('close', function() {
        mainWindow = null;
    });
    
++  setTimeout(() => {
++      app.dock.bounce();
++  }, 5000);
    
    mainWindow.on('blur', () => {
        const badgeString = app.dock.getBadge();
        if (badgeString === '') {
            app.dock.setBadge('1');
        } else {
          app.dock.setBadge((parseInt(badgeString) + 1).toString());
        }
    });
}

弹跳效果如图所示,注意让应用处于失去焦点的状态可以看到这个效果:

8

到这里,自定义 Dock 图标这个部分就介绍完了。

webpack 第三章插件eslint

老师, 刚安装完eslint以后就提醒我webpack里面安装的各种插件应该放在dependencies而不是
devDependencies, 我看教程里面全是-D, 那应该放在哪里呢?

webpack 项目和依赖如何分开打包

老师我之前在学习第33课的时候按照您的办法打包, 没有任何问题.
后面我自己打包了一个 react 组件包, 想发布在 npm 上安装到其他项目直接引入使用, 然而 react 报错说引入的东西太大了, 后面发现打包出来只有一个 js 文件, 它包含了我所有的依赖包和项目文件(总共有6MB, 即使压缩了也是2.6MB).
之后我尝试配置 webpack.config.dll.js 将 react, react-dom, antd 这种大的依赖包打包, 在其他 webpack 配置中通过 webpack.DllReferencePlugin 引入, 最后打包成功, 然而发布在 npm 上的时候, 这部分文件没有发布上去.
我想问的是:

  1. 有没有什么更好的办法将项目文件和依赖包分开来打包?
  2. 通过 webpack.config.dll.js 打包的 static 目录下的文件, 如何能够一并发布在 npm 上?

chapter 3 ssr

老师我跟着你操作以后一直渲染不出页面, 然后就直接用你的课件去操作.
先npm i, 然后 npm run build:ssr, 在node server/index.js, 就出现以下情况:

get_error
这是什么原因呢? (第三章34-35服务端渲染部分)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.