本篇文章会系统的介绍下 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.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;
}
最后
下面是本篇文章的代码演示资料,如果有需求,可以自行获取。
- inline-resource demo 演示
- inline-html-loader 源码
- inline-file-loader 源码
我的个人博客:https://github.com/cpselvis/b…
想学习更多干货内容可以扫码关注我的公众号:推送频率每周一篇