Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

webpack4 中如何实现资源内联? #5

Open
cpselvis opened this issue Oct 5, 2019 · 5 comments
Open

webpack4 中如何实现资源内联? #5

cpselvis opened this issue Oct 5, 2019 · 5 comments
Labels

Comments

@cpselvis
Copy link
Owner

cpselvis commented Oct 5, 2019

在专栏课程里,关于 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 组件) 的思路很简单,就是直接读取某个文件的内容,然后插入到对应的位置。我们可以借助 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.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;
}

最后

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

@SWbeginner
Copy link

点个赞

@jhmei
Copy link

jhmei commented Jun 2, 2020

厉害了

1 similar comment
@xiaoliu6
Copy link

厉害了

@Chorer
Copy link

Chorer commented Aug 18, 2021

ejs 语法是 <%= %> 不是 ${},这里要修改一下

@ajl512
Copy link

ajl512 commented Sep 21, 2023

我这边发现一个问题,产生的css产物中,backgroud:url(../img/bg.png)原封不动的内联到了html中,而html和img文件夹是并列关系,所以会出现找不到图片的情况; 请问如何解决?
我试着将产物css改成和html并列,里面依旧还是backgroud:url(../img/bg.png);使用url-loaderfile-loader无论如何设置,产物中所有的背景图均转换为了base64图片; 关闭又无法解决路径问题;
项目是vue-cli项目;css使用的是stylus语法

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants