本篇文章会系统的介绍下 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-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 组件) 的思路很简单,就是直接读取某个文件的内容,然后插入到对应的位置。我们可以借助 raw-loader@0.5.1版本,最新的 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.jsconst 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.jsconst 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;}

最后

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

  • inline-resource demo演示
  • inline-html-loader 源码
  • inline-file-loader 源码

我的个人博客:https://github.com/cpselvis/b...

想学习更多干货内容可以扫码关注我的公众号:推送频率每周一篇