一、PostCSS插件自动补齐CSS3前缀
1.1 背景
CSS3属性为什么有前缀?因为浏览器标准没有统一。
比如下面,写起来很慢,低效,麻烦。
.box {
-moz-border-radius: 10px;
-webkit-border-radius: 10px;
-o-border-radius: 10px;
border-radius: 10px;
}
1.2 解决
autoprefixer,css的后置处理器,这个autoprefixer和less、sass不一样,less、sass是预处理器,预处理器是打包前处理,而autoprefixer是后置处理器,在样式处理好后,代码最终生成完了之后,再进行后置处理。
根据Can I Use规则(https://caniuse.com/)
autoprefixer通常和postcss-loader一起用
npm install postcss-loader autoprefixer -D
我们在less文件中写一个display: flex; 然后看编译之后是否带了各种前缀
全文配置:
const path = require('path')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
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: /\.js$/,
use: 'babel-loader'
},
{
test: /\.css$/,
use: [
// 'style-loader',
MiniCssExtractPlugin.loader,
'css-loader'
]
},
{
test: /\.less$/,
use: [
// 'style-loader',
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader',
{
loader: 'postcss-loader',
options: {
plugins: () => [
require('autoprefixer')({
browsers: ['last 2 version', '>1%', 'ios 7']
})
]
}
}
]
},
{
test: /\.(png|jpg|gif|jpeg)$/,
use: [{
loader: 'file-loader',
options: {
name: '[name]_[hash:8].[ext]'
}
}]
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [{
loader: 'file-loader',
options: {
name: '[name]_[hash:8].[ext]'
}
}]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new MiniCssExtractPlugin({
filename: '[name]_[contenthash:8].css'
}),
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano')
}),
// 这里多页面,需要写两个
new HtmlWebpackPlugin({
template: path.join(__dirname, 'src/index.html'),
filename: 'index.html',
chunks: ['index'],
inject: true,
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
removeComments: false
}
}),
new HtmlWebpackPlugin({
template: path.join(__dirname, 'src/search.html'),
filename: 'search.html',
chunks: ['search'],
inject: true,
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
removeComments: false
}
})
]
}
结果如下,不负众望。
但是我们看到了一下警告:
二、移动端CSS px转换为rem
2.1 分辨率
我们可以借助媒体查询去适配,但是缺陷是要兼顾很多,需要多套适配样式代码
@media screen and (max-width: 980px) {
.header {
width: 900px;
}
}
@media screen and (max-width: 480px) {
.header {
height: 400px;
}
}
@media screen and (max-width: 350px) {
.header {
height: 300px;
}
}
2.2 rem是什么
w3c对rem的定义: font-size of the root element
rem和px的对比:
rem: 相对单位
px:绝对单位
说一下我个人的理解吧。
px:理解为将屏幕分成的每一份的“长度”,比如,360px,就是将该设备宽度平均分成360份。我们拿到设计稿之后,设计稿只有一个数值,比如750px的设计稿,但是我们移动端的手机可能是360px,可能是414的,可能是460的,375的,等等。我们总不能都按照750设计稿上的数字去写吧。
所以,我们把750px的设计稿理解为设计师把设备平均分成750份,比如设计稿里面某个div宽度是100px,我们理解为这个div占了750份里面的100份。
那么,推广应用一下,无论我们的设备是多少分辨率的,也就是宽度不管是360的,还是414的,我们只要获取到屏幕宽度即可window.screen.width,或者还有其他方法来获取,具体看情况吧,反正肯定能拿到手机屏幕的宽度。我们令 屏幕宽度 screenWidth = window.screen.width吧。我们按照设计师的意图,将屏幕宽度分成750份。那每一份就是 screenWidth / 750 , 那么设计稿里面100px的div实际宽度就是 100 * (screenWidth/750), 其实每次这么算其实也是很不方便的,因为可能出现一些奇奇怪怪的数字,让你不能一眼看出到底是多大。
我们已经知道,每一份是 screenWidth/750,这个数值可能是个小数,是个奇奇怪怪的数字,不好计算,我们进一步想想,我们可以把多份合在一起,合成一个比较整或者比较好计算的数字,比如,我们可以把75份合在一起, 那就是 75 * (screenWidth/750) = screenWidth/10, 我们把这个数字叫做根字号,可见根字号是可以自己设计定制的,你可以根据自己的喜好来规定。
根字号 = x * (screenWidth/750)
其中,x就是x份的屏幕宽度,我们把这个x份叫做一个rem。也就是1个rem包含了多少份。因为,x的值你可以根据自己的喜好来定,所以,这个rem也是一个很灵活的值。另外,说明一点,浏览器默认的值是1rem为16份。
2.3 解决方案
使用px2rem-loader,将px转换成rem,px转换成rem之后,我们需要知道一个rem到底等于多少个px?这个时候,我们需要在页面打开的时候,动态计算根元素的font-size的值。我们可以利用手机淘宝里一个很成熟的方案,是一个库,lib-flexible。这个库能自动根据当前的设备的宽高,自动计算根元素的实际font-size值。
https://github.com/amfe/lib-flexible
npm i px2rem-loader -D
npm i lib-flexible -S
我们先运行一下:
接下来,我们要将根元素相对的rem计算出来,也就是根元素,html这个结点在实际的设备分辨率中的font-size大小。这个时候,我们需要使用lib-flexible,但是,我们现在要把这个库引进来,暂时手动将这个代码引进来吧。
在src/search.html中加入.我们在node_modules里找到 lib-flexible这个库,将里面的源代码复制粘贴到search.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>
<script type="text/javascript">
;(function(win, lib) {
var doc = win.document;
var docEl = doc.documentElement;
var metaEl = doc.querySelector('meta[name="viewport"]');
var flexibleEl = doc.querySelector('meta[name="flexible"]');
var dpr = 0;
var scale = 0;
var tid;
var flexible = lib.flexible || (lib.flexible = {});
if (metaEl) {
console.warn('将根据已有的meta标签来设置缩放比例');
var match = metaEl.getAttribute('content').match(/initial\-scale=([\d\.]+)/);
if (match) {
scale = parseFloat(match[1]);
dpr = parseInt(1 / scale);
}
} else if (flexibleEl) {
var content = flexibleEl.getAttribute('content');
if (content) {
var initialDpr = content.match(/initial\-dpr=([\d\.]+)/);
var maximumDpr = content.match(/maximum\-dpr=([\d\.]+)/);
if (initialDpr) {
dpr = parseFloat(initialDpr[1]);
scale = parseFloat((1 / dpr).toFixed(2));
}
if (maximumDpr) {
dpr = parseFloat(maximumDpr[1]);
scale = parseFloat((1 / dpr).toFixed(2));
}
}
}
if (!dpr && !scale) {
var isAndroid = win.navigator.appVersion.match(/android/gi);
var isIPhone = win.navigator.appVersion.match(/iphone/gi);
var devicePixelRatio = win.devicePixelRatio;
if (isIPhone) {
// iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案
if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
dpr = 3;
} else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
dpr = 2;
} else {
dpr = 1;
}
} else {
// 其他设备下,仍旧使用1倍的方案
dpr = 1;
}
scale = 1 / dpr;
}
docEl.setAttribute('data-dpr', dpr);
if (!metaEl) {
metaEl = doc.createElement('meta');
metaEl.setAttribute('name', 'viewport');
metaEl.setAttribute('content', 'initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
if (docEl.firstElementChild) {
docEl.firstElementChild.appendChild(metaEl);
} else {
var wrap = doc.createElement('div');
wrap.appendChild(metaEl);
doc.write(wrap.innerHTML);
}
}
function refreshRem(){
var width = docEl.getBoundingClientRect().width;
if (width / dpr > 540) {
width = 540 * dpr;
}
var rem = width / 10;
docEl.style.fontSize = rem + 'px';
flexible.rem = win.rem = rem;
}
win.addEventListener('resize', function() {
clearTimeout(tid);
tid = setTimeout(refreshRem, 300);
}, false);
win.addEventListener('pageshow', function(e) {
if (e.persisted) {
clearTimeout(tid);
tid = setTimeout(refreshRem, 300);
}
}, false);
if (doc.readyState === 'complete') {
doc.body.style.fontSize = 12 * dpr + 'px';
} else {
doc.addEventListener('DOMContentLoaded', function(e) {
doc.body.style.fontSize = 12 * dpr + 'px';
}, false);
}
refreshRem();
flexible.dpr = win.dpr = dpr;
flexible.refreshRem = refreshRem;
flexible.rem2px = function(d) {
var val = parseFloat(d) * this.rem;
if (typeof d === 'string' && d.match(/rem$/)) {
val += 'px';
}
return val;
}
flexible.px2rem = function(d) {
var val = parseFloat(d) / this.rem;
if (typeof d === 'string' && d.match(/px$/)) {
val += 'rem';
}
return val;
}
})(window, window['lib'] || (window['lib'] = {}));
</script>
</head>
<body>
<div id="root"></div>
</body>
</html>
npm run build之后,打开 dist/search.html文件
看到了吗,字体也会随着屏幕分辨率的变化而变化
2.4 答疑
2.4.1 这个最大的缺点就是会把第三方ui库的px也给转了
答: 这个px2rem-loader 也是可以设置 exclude 的,可以把 node_modules 里面的模块 exclude 掉。
另外如果不设置 exclude,那么也可以使用 /no/的语法去设置某一行样式不进行 px2rem 的转换操作。
可以用 /no/ 这种注释语法。比如:
.page {
font-size: 12px; /*no*/
width: 375px; /*no*/
height: 40px;
}
后面有 /no/这种注释语法会不进行 rem 的转换
2.4.2 按照750的设计稿,直接就是10px写10px吗?为什么看着比设计稿的要大?
答: 如果设置的 remUnit 是 75,那么对于 750 的设计稿如果字体是 24px,就写 24px(实际上在 iphone 6是12px的大小)
如果设置的 remUnit 是 37.5,那么对于 375的设计稿如果字体是 12px,就写 12px(实际上在 iphone 6是12px的大小)
看着比设计稿的要大这个需要以 iphone 6为参照哈
2.4.3 flexible.js作用是什么?使用px2rem-loader,就必须和flexible.js一起搭配使用吗?
答:必须一起使用。
px2rem-loader 只是以构建的手段将 px 单位转换成了 rem。但是 rem 和 px 的单位计算并不清楚,flexible.js 的作用就是动态的去计算不同设备下的 rem 相对 px 的单位,也就是计算跟元素 html 节点的 font-size 大小
三、静态资源内联
上面我们已经发现,为了引入一个lib-flexible,我们还要将代码复制粘贴到HTML文件中,这显然是不符合现代开发逻辑的,不够自动化。
内联,比如我们怎么将css 代码、js代码内联到HTML中,怎么将图片、字体内联到代码中等
3.1 内联的意义
- 页面框架的初始化脚本,比如上一节里面lib-flexible,项目初始化就要执行的脚本。
- 上报相关打点,css初始化和css加载完成这些上报点的代码都要内联到HTML中。
- css内联避免页面闪动。
- 减少http网络请求数,小图片或者字体内联url-loader
3.2 raw-loader内联html
比如要把一段html片段内联进去,这个html片段是移动端开发要用的meta信息片段,很大一段,很多页面都要用到。
<script>${require('raw-loader!babel-loader!./xx.js')}</script>
我们准备了一个meta.html文件,用于内联到其他html文件中,尤其是做一些手机端网页,都需要一些meta信息。
<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/">
npm install raw-loader@0.5.1 -D
我们在src/search.html模板修改如下,不再使用
<!DOCTYPE html>
<html lang="en">
<head>
${ require('raw-loader!./meta.html') }
<title>Document</title>
<script>${require('raw-loader!babel-loader!../../node_modules/lib-flexible/lib-flexible.js')}</script>
</head>
<body>
<div id="root"></div>
</body>
</html>
四、多页面打包通用方案
4.1 什么是多页面MPA
发布上线后有很多个入口,一个页面就是一个业务,单页面应用就是只有一个入口,所有的业务都放在一个入口,不同的业务用同一个url,只是后面hash不一样。多页应用好处就是多个页面解耦,而且对SEO友好。
每个页面对应一个entry,一个html-webpack-plugin,缺点是每次新增或者删除页面需要改webpack配置。
4.2 解决方案
动态获取entry和设置html-webpack-plugin数量
利用glob.sync
entry: glob.sync(path.join(__dirname, './src/*/index.js'))
传统的是:
module.exports = {
entry: {
index: './src/index/index.js',
search: './src/search/index.js'
}
}
当然这样做,我们需要一些约定
- 都放在src下
- 不同的目录的出口文件都命名为index.js
就是利用glob库
entry: glob.sync(path.join(__dirname, './src/*/index.js'))
__dirname就是项目根目录,根目录下的src下匹配src下所有一级目录,然后继续匹配到下面的index.js,这样所有的entry都动态的获取出来了,我们根据这个数量动态的设置webpack中 html-webpack-plugin的数量。
4.3 操作举例
4.3.1 修改目录和文件名
新建两个目录:index 和 search,把属于他们的文件都move到相应的目录中,同时,入口文件都修改为index.js.
4.3.2 安装 glob
npm install glob -D
4.3.3 在webpack中改写
const glob = require('glob')
// 动态设置多页面的函数
const setMPA = () => {
const myEntry = {}
const myHtmlWebpackPlugin = []
const entryFiles = glob.sync(path.join(__dirname, './src/*/index.js'))
// entryFiles = [
// '/Users/guoyu/work/webpack-demo/src/index/index.js',
// '/Users/guoyu/work/webpack-demo/src/search/index.js'
// ]
Object.keys(entryFiles).forEach(index => {
const entryFile = entryFiles[index]
const match = entryFile.match(/src\/(.*)\/index\.js/)
// index === 0
// match == [
// 'src/index/index.js',
// 'index',
// index: 31,
// input: '/Users/guoyu/work/webpack-demo/src/index/index.js',
// groups: undefined
// ]
// index === 1
// match == [
// 'src/search/index.js',
// 'search',
// index: 31,
// input: '/Users/guoyu/work/webpack-demo/src/search/index.js',
// groups: undefined
// ]
const pageName = match && match[1]
myEntry[pageName] = entryFile
myHtmlWebpackPlugin.push(
new HtmlWebpackPlugin({
template: path.join(__dirname, `src/${pageName}/index.html`),
filename: `${pageName}.html`,
chunks: [pageName],
inject: true,
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
removeComments: false
}
})
)
})
return {
entry: myEntry,
htmlWebpackPlugin: myHtmlWebpackPlugin
}
}
// 直接调用
const { entry, htmlWebpackPlugin } = setMPA()
module.exports = {
entry: entry,
plugins: [
new CleanWebpackPlugin(),
new MiniCssExtractPlugin({
filename: '[name]_[contenthash:8].css'
}),
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano')
})
].concat(htmlWebpackPlugin) // 填充到webpack的plugins中
}
4.3.4 运行结果
五、使用source map
5.1 简介
作用:通过source map定位到源代码,可以看看阮一峰老师的科普文。
开发环境开启,线上环境关闭,因为如果线上环境开启会暴露业务逻辑,是不安全的。
线上排查的时候,可以把sourcemap上传到错误监控系统。
eval: 使用eval包裹模块代码
sourcemap: 产生.map文件
cheap:不包括列信息
inline:将.map做为DataURL嵌入,不单独产生.map文件
module:包含loader的source map
source map 类型如下,其实就是上面五种的各种组合。
5.2 操作实践
先把webpack中的mode设置为none,因为如果是production,会压缩,不利于观察
mode: 'none'
再在webpack中添加一项
devtool: 'eval'
npm run build之后观察打包出来的文件
等等,有很多模块,很多个eval
我们可以看到每个eval后面有个sourceURL,指明了编译的是哪个模块
eval 换成 source-map
devtool: 'source-map'
换成 inline-map
devtool: 'inline-source-map'
看看构建结果,没有了.map文件,而且构建出来的js文件也比source-map大了很多,因为inline-source-map把map文件打到js文件中了,最后一行把map文件inline进来了,最后一行很长很长
下面我们把动态设置多页面的代码添加到webpack.dev.js中,但是暂时先不设置devtool, 我们在search/index.js中加个断点
npm run dev :http://localhost:8080/search.html
这种很难调试
现在我们在webpack.dev.js中添加一句:devtool: source-map
npm run dev, http://localhost:8080/search.html,这种就非常好调试了,直接调试源代码。
我们把 source-map 换为 cheap-source-map,只能看到错误的行数
devtool: 'cheap-source-map'
六、提取页面公共资源
项目中有很多页面,这些页面使用的基础库一般都是一样的,有些页面可能还有一些公共的模块,打包的时候,如果把每个公共模块在每个页面都打一份,就很浪费,体积很大。
6.1 基础库分离splitChunksPlugin
splitChunksPlugin这个库是webpack4.x内置的,用于替换淘汰掉之前的CommonChunkPlugin
chunks参数说明:
1,async:异步引入的库才进行分离(动态import一个react库等等),同步引入的忽略(默认)
2,initial:同步引入的库进行分离,同步引入的库,如果有相同的,就进行分离,抽离出一个chunk出来
3,all:所有引入的库进行分离(推荐)
minChunks说明:
minChunks: 2 , 是指至少有2个地方用到了同一段代码,这一段代码就要被打包分离出一个chunk。专业点说就是设置最小引用次数。
maxInitalRequest说明:
maxInitalRequest: 3, 是指你通过插件分离出了4或者5个chunk,浏览器同时去请求的一个数量
minSize:分离的包提交大小。
下面通过实例,将react和react-dom公共资源包引进来,打包的时候,不用分析react和react-dom这种基础库。
6.2 操作实践
先看看html-webpack-externals-plugin的用法,配合cdn
npm i html-webpack-externals-plugin -D
webpack.prod.js添加代码:
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin')
// plugins数组添加一项
new HtmlWebpackExternalsPlugin({
externals: [{
module: 'react',
entry: 'https://now8.gtimg.com/now/lib/16.2.0/react.min.js',
global: 'React'
}, {
module: 'react-dom',
entry: 'https://now8.gtimg.com/now/lib/16.2.0/react-dom.min.js',
global: 'ReactDOM'
}]
})
附全部webpack.prod.js代码
const path = require('path')
const glob = require('glob')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin')
// 动态设置多页面的函数
const setMPA = () => {
const myEntry = {}
const myHtmlWebpackPlugin = []
const entryFiles = glob.sync(path.join(__dirname, './src/*/index.js'))
// entryFiles = [
// '/Users/guoyu/work/webpack-demo/src/index/index.js',
// '/Users/guoyu/work/webpack-demo/src/search/index.js'
// ]
Object.keys(entryFiles).forEach(index => {
const entryFile = entryFiles[index]
const match = entryFile.match(/src\/(.*)\/index\.js/)
const pageName = match && match[1]
myEntry[pageName] = entryFile
myHtmlWebpackPlugin.push(
new HtmlWebpackPlugin({
template: path.join(__dirname, `src/${pageName}/index.html`),
filename: `${pageName}.html`,
chunks: [pageName],
inject: true,
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
removeComments: false
}
})
)
})
return {
entry: myEntry,
htmlWebpackPlugin: myHtmlWebpackPlugin
}
}
const { entry, htmlWebpackPlugin } = setMPA()
module.exports = {
entry: entry,
output: {
path: path.join(__dirname, 'dist'),
filename: '[name]_[chunkhash:8].js'
},
mode: 'none',
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
},
{
test: /\.css$/,
use: [
// 'style-loader',
MiniCssExtractPlugin.loader,
'css-loader'
]
},
{
test: /\.less$/,
use: [
// 'style-loader',
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader',
{
loader: 'postcss-loader',
options: {
plugins: () => [
require('autoprefixer')
]
}
},
{
loader: 'px2rem-loader',
options: {
// 这里设置1个rem为75px,可以自己根据设备设置
remUnit: 75,
// 转换成rem后,保留8位小数
remPrecision: 8
}
}
]
},
{
test: /\.(png|jpg|gif|jpeg)$/,
use: [{
loader: 'file-loader',
options: {
name: '[name]_[hash:8].[ext]'
}
}]
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [{
loader: 'file-loader',
options: {
name: '[name]_[hash:8].[ext]'
}
}]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new MiniCssExtractPlugin({
filename: '[name]_[contenthash:8].css'
}),
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano')
}),
new HtmlWebpackExternalsPlugin({
externals: [{
module: 'react',
entry: 'https://now8.gtimg.com/now/lib/16.2.0/react.min.js',
global: 'React'
}, {
module: 'react-dom',
entry: 'https://now8.gtimg.com/now/lib/16.2.0/react-dom.min.js',
global: 'ReactDOM'
}],
files: ['search.html']
})
].concat(htmlWebpackPlugin),
devtool: 'inline-source-map'
}
下面,我们看看如何用splitChunksPlugin,这是webpack4.x内置的,可以直接配置。先把上面的HtmlWebpackExternalsPlugin代码去掉,使用splitChunksPlugin来把react,react-dom分离成一个vendor。
下面是完整配置,只用关注第24行和122-128行
const path = require('path')
const glob = require('glob')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
// 动态设置多页面的函数
const setMPA = () => {
const myEntry = {}
const myHtmlWebpackPlugin = []
const entryFiles = glob.sync(path.join(__dirname, './src/*/index.js'))
Object.keys(entryFiles).forEach(index => {
const entryFile = entryFiles[index]
const match = entryFile.match(/src\/(.*)\/index\.js/)
const pageName = match && match[1]
myEntry[pageName] = entryFile
myHtmlWebpackPlugin.push(
new HtmlWebpackPlugin({
template: path.join(__dirname, `src/${pageName}/index.html`),
filename: `${pageName}.html`,
chunks: ['gyVendors', pageName],//将splitChunks提取的vendors(gyVendors)加上
inject: true,
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
removeComments: false
}
})
)
})
return {
entry: myEntry,
htmlWebpackPlugin: myHtmlWebpackPlugin
}
}
const { entry, htmlWebpackPlugin } = setMPA()
module.exports = {
entry: entry,
output: {
path: path.join(__dirname, 'dist'),
filename: '[name]_[chunkhash:8].js'
},
mode: 'production',
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
},
{
test: /\.css$/,
use: [
// 'style-loader',
MiniCssExtractPlugin.loader,
'css-loader'
]
},
{
test: /\.less$/,
use: [
// 'style-loader',
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader',
{
loader: 'postcss-loader',
options: {
plugins: () => [
require('autoprefixer')
]
}
},
{
loader: 'px2rem-loader',
options: {
// 这里设置1个rem为75px,可以自己根据设备设置
remUnit: 75,
// 转换成rem后,保留8位小数
remPrecision: 8
}
}
]
},
{
test: /\.(png|jpg|gif|jpeg)$/,
use: [{
loader: 'file-loader',
options: {
name: '[name]_[hash:8].[ext]'
}
}]
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [{
loader: 'file-loader',
options: {
name: '[name]_[hash:8].[ext]'
}
}]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new MiniCssExtractPlugin({
filename: '[name]_[contenthash:8].css'
}),
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano')
})
].concat(htmlWebpackPlugin),
// 配置splitChunks分离公共包,这是webpack4.x内置的一个插件
optimization: {
splitChunks: {
cacheGroups: {
commons: {
test: /(rect|react-dom)/,
name: 'gyVendors', // 记得在HtmlWebpackPlugin中把gyVendors加上
chunks: 'all'
}
}
}
}
}
下面我们进一步探索 splitChunks的用法,我们手写一个公共函数,让search和index两个业务页面都用上这个公共函数commom/index.js
webpack.prod.js配置如下:
optimization: {
splitChunks: {
minSize: 0,//只要是公共包,就抽离出来
cacheGroups: {
commons: {
name: 'commons', // 记得在HtmlWebpackPlugin中把commons加上
chunks: 'all',
minChunks: 2 // 公共包至少被引用了两次才会被抽离出来
}
}
}
}
全配如下:
const path = require('path')
const glob = require('glob')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
// 动态设置多页面的函数
const setMPA = () => {
const myEntry = {}
const myHtmlWebpackPlugin = []
const entryFiles = glob.sync(path.join(__dirname, './src/*/index.js'))
Object.keys(entryFiles).forEach(index => {
const entryFile = entryFiles[index]
const match = entryFile.match(/src\/(.*)\/index\.js/)
const pageName = match && match[1]
myEntry[pageName] = entryFile
myHtmlWebpackPlugin.push(
new HtmlWebpackPlugin({
template: path.join(__dirname, `src/${pageName}/index.html`),
filename: `${pageName}.html`,
chunks: ['commons', pageName], // 将splitChunks提取的commons加上
inject: true,
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
removeComments: false
}
})
)
})
return {
entry: myEntry,
htmlWebpackPlugin: myHtmlWebpackPlugin
}
}
const { entry, htmlWebpackPlugin } = setMPA()
module.exports = {
entry: entry,
output: {
path: path.join(__dirname, 'dist'),
filename: '[name]_[chunkhash:8].js'
},
mode: 'production',
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
},
{
test: /\.css$/,
use: [
// 'style-loader',
MiniCssExtractPlugin.loader,
'css-loader'
]
},
{
test: /\.less$/,
use: [
// 'style-loader',
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader',
{
loader: 'postcss-loader',
options: {
plugins: () => [
require('autoprefixer')
]
}
},
{
loader: 'px2rem-loader',
options: {
// 这里设置1个rem为75px,可以自己根据设备设置
remUnit: 75,
// 转换成rem后,保留8位小数
remPrecision: 8
}
}
]
},
{
test: /\.(png|jpg|gif|jpeg)$/,
use: [{
loader: 'file-loader',
options: {
name: '[name]_[hash:8].[ext]'
}
}]
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [{
loader: 'file-loader',
options: {
name: '[name]_[hash:8].[ext]'
}
}]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new MiniCssExtractPlugin({
filename: '[name]_[contenthash:8].css'
}),
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano')
})
].concat(htmlWebpackPlugin),
// 配置splitChunks分离公共包,这是webpack4.x内置的一个插件
optimization: {
splitChunks: {
minSize: 0,
cacheGroups: {
commons: {
// test: /(rect|react-dom)/,
name: 'commons', // 记得在HtmlWebpackPlugin中把commons加上
chunks: 'all',
minChunks: 2
}
}
}
}
}
npm run build之后
上面看到commons_d5edc0d8.js 180 bytes,而且是search和index两个地方用到:那么我们把
minSize: 0换成 minSize: 10000 再打包,你会发现commons_xxxxxxxx.js不见了,因为不满足min配置
同样
minChunks: 2换成 minChunks: 3 再打包,你会发现commons_xxxxxxxx.js不见了,因为不满足min配置
6.3 结合webpack-bundle-analyzer分析
npm install webpack-bundle-analyzer -D
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
// plugins里添加如下的插件
new BundleAnalyzerPlugin()
package.json添加如下:
"scripts": {
"build": "webpack --config webpack.prod.js",
"watch": "webpack --watch",
"dev": "webpack-dev-server --config webpack.dev.js --open",
// 下面一句才是重点
"analyz": "analyz=true npm run build"
}
直接看代码
optimization: {
splitChunks: {
minSize: 0,
cacheGroups: {
vendors: {
test: /(react|react-dom)/,
name: 'vendors',
chunks: 'all',
priority: -10
},
commons: {
name: 'commons',
chunks: 'all',
minChunks: 2,
priority: -20
},
ruok: {
test: /[\\/]node_modules[\\/]/,
name: 'nModules',
chunks: 'all',
priority: -30
},
default: {
name: 'guoyu-default',
chunks: 'all',
minChunks: 2,
priority: -40
}
}
}
}
我们看看上面的webpack配置代码,我们源码中的search/index.js和index/index.js都引用了common/index.js。
这种情况既满足上面webpack配置中的commons也满足default。
那么既然common/index.js既满足commons又满足default,那么common/index.js到底会被打包到哪个chunk呢?答案就是 priority,priority谁的大,就打包到谁里面。上面,commons的priority大。我们看看结果。
我们把default的priority改为-10,大于commons的-20,再次分析
6.4 问答
Q1:如果我既想提取静态资源react,又想提取公共资源还想打到不同的文件里怎么办,支持数组方式吗?
A1:可以的,这个cacheGroup 可以配置多个组的。想打包到不同的文件只需要传到 html-webpack-plugin的 chunk 按照需要设置即可
Q2:同时分离基础库和公共脚本,具体要怎么配呢?期望提取出的common公共脚本 + vendor基础库,都可以自定义名称。但是貌似不行。。打包出来的vendor,总是vendors~xxx_hash.js这样的。而且设置两次name,第二次的会覆盖掉第一次的,配置如下:
cacheGroups: {
commons: {
name: 'common',
chunks: 'all',
minChunks: 2
},
vendors: {
test: /(react|react-dom)/,
name: 'vendor',
chunks: 'all'
}
}
A2:正确配置如下:
optimization: {
splitChunks: {
minSize: 0,
cacheGroups: {
vendors: {
test: /(react|react-dom)/,
name: 'vendors',
chunks: 'all',
priority: -10
},
commons: {
name: 'commons',
chunks: 'all',
minChunks: 2,
priority: -20
}
}
}
}
Q3:priority 是什么意思?
A3:优先级权重,使用一个数字表示,数字越大,表示优先级越高,通俗的说,就是如果有一个模块满足了多个缓存组的条件就会去按照权重划分,谁的权重高就优先按照谁的规则处理。比如,一个模块既满足上面A2中vendors的分组要求,也满足commons的分组要求,谁的权重大,就打包到谁的分组。
七、摇树优化TreeShaking
7.1 概念
一个模块可能有多个方法,只要其中的某个方法使用到了,则整个文件都会被打到bundle里,tree shaking就是只把用到的方法打入bundle,没用到的方法在uglify阶段被擦除掉。
tree shaking在webpack4.x默认支持,mode设置为production的情况默认开启
但是,使用treeshaking 必须是ES6语法,cjs语法不支持。
DEC(Dead Code Elimination)
DEC:擦除淘汰无用代码
1,代码不会被执行,不可到达
if (false) {
console.log('......这段代码永远不会被执行......')
}
2,代码执行的结果不会被用到
3,代码只会影响死变量(只写不读)
7.2 treeShaking的原理
主要是利用了ES6模块的特点:
- 只能作为模块顶层的语句出现(import、export语句都在文件顶层出现)
- import 的模块只能是字符串常量
- import binding 是immutable的(import一个模块后,不能被修改)
代码擦除:uglify阶段删除无用代码
所以,commonJS是不具备上面三个特点的,因为可以动态require,不同条件require不同的模块,这都是cjs写法。为什么?treeshaking最本质的还是对模块代码的静态分析,在编译阶段,哪些代码会被用到都是要确定下来的,不是说在代码运行的时候再去分析哪些代码有没有用到,不能这么操作。哪些代码没用到,treeshaking会将这些代码标识出来,uglify阶段删除无用代码。
7.3 treeShaking实际操作
1,我们在search文件夹下,创建一个search/tree-shaking.js文件内容如下:
export function a() {
return 'this is func a'
}
export function b() {
return 'this is func b'
}
2,在search/index.js中只引入a方法
import { a } from './tree-shaking'
3,webpack.prod.js将mode换成’none’,因为之前的’production’默认开启了treeShaking
mode: 'none'
4,执行npm run build
从上图可以看到,打包出来的,没有开启treeShaking,这两个代码都打包进去了,尽管我们都没用到。
5,下面,我们none换为 production
可见,根本没打进去,无用代码被TreeShaking掉了。
那我们实际用一下a,在search/index.js中做如下改动
编译打包之后
用的着的留下了,用不着的摇走了。
再看一个例子
npm run build
都没被打进包里。
八、Scope Hoisting使用和原理分离
8.1 背景
构建后的代码存在大量的闭包代码
比如,新建两个common/a.js和common/b.js两个文件。在src/index/index.js中引用
a.js和b.js分别是
export function testA() {
console.log('this is testA..........')
}
export function testB() {
console.log('this is testB..........')
}
把webpack.dev.js中的mode换为none,不然是压缩混淆代码,看不清
mode: 'none'
一个项目,可能有几百上千个包裹。
1,大量函数闭包包裹代码,导致体积增大,模块越多越明显。
2,都是通过闭包方式,运行代码时候,创建的函数作用域变多,内存开销变大。
下面我们看看打包模块
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "testA", function() { return testA; });
function testA() {
console.log('this is testA..........');
}
/***/ }),
/* 2 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "testB", function() { return testB; });
function testB() {
console.log('this is testB..........');
}
/***/ })
模块初始化函数:就是webpack对模块进行处理,增加的包裹
我们可以看到
- 被import进来的,被webpack转换后的模块都会带上一层包裹
- import会被转换为 webpack_require
进一步分析一下:
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _common_a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
/* harmony import */ var _common_b__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2);
// import _ from 'lodash'
// import { commonTest } from '../../common'
// function component() {
// commonTest('Index')
// let ele = document.createElement('div')
// ele.innerHTML = _.join(['Hello', 'guoyu'], ' ')
// return ele
// }
// document.body.appendChild(component())
Object(_common_a__WEBPACK_IMPORTED_MODULE_0__["testA"])();
Object(_common_b__WEBPACK_IMPORTED_MODULE_1__["testB"])();
/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "testA", function() { return testA; });
function testA() {
console.log('this is testA..........');
}
/***/ }),
/* 2 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "testB", function() { return testB; });
function testB() {
console.log('this is testB..........');
}
/***/ })
/******/ ]);
从上面打包出来的文件可以看出:
- 打包出来的是个匿名闭包
- modules是一个数组,每一项都是一个模块初始化函数
- __webpack_require__用来加载模块,返回module.exports
- 通过webpack_require_method(0)启动程序
分析一个函数:
function __webpack_require__(moduleId) {
// Check if module is in cache
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// Execute the module function
// require另一个模块(依赖),会传递如下参数
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Flag the module as loaded
module.l = true;
// Return the exports of the module
return module.exports;
}
8.2 原理
在上面了解了webpack模块机制之后,我们看看有么有办法对前面提到的包裹代码有所优化?
scope hoisting和treeShaking一样,都是从rollup中借鉴过来的。
将所有模块的代码按照引用顺序放在一个函数作用域里面,然后适当的重命名一些变量,以防止变量名冲突。
通过scop hoisting 可以减少函数声明代码和内存开销
scope hoisting 是webpack3提出的,翻译过来就是“作用域提升”,webpack会把js文件提升到引入者的顶部。
在之前,如果a模块引用了b模块,那么我们可以把a模块放在b模块前面,a调用b,但他们之间有模块包裹,所以并不影响,如果要消除这种包裹代码的话,我们根据模块的引用顺序,进行位置排放,也就是我们要把被引用的b模块放在a模块前面,那么a就可以直接读取b的内容,这样就消除了包裹,减少了内存开销和函数申明代码。
在webpack4.x中,scope hoisting 在mode为production时默认开启。
但是在webpack3.x中还是要手动配置的。下面是webpack3.x中的配置:
// webpack3.x
plugins: [
new webpack.optimize.ModuleConcatenationPlugin() // 开启scope hoisting
]
在webpack4.x中,scope hoisting 在mode为production时默认开启。
必须是ES6语法,cjs不支持,因为scope hoisting是一个静态分析过程,cjs是一个动态引入模块的方式,没办法进行静态分析,比如分析模块的引入顺序。
8.3 使用
首先,禁止掉production,把mode设置为none,看看打包结果。
npm run build 看看。我们回到 8.1 贴出来的打包后的代码,我们没有开启scope hoisting的时候,可以看到如果模块多,包裹数量也会越来越多。坏处显而易见。
接下来,我们使用scope hoisting功能,看看打包后的效果。但是,我们不能草率的把mode设置为production,因为这样会uglify代码,都是压缩混淆的,看的不直观。
所以,我们依旧保持 mode 为 ‘none’,但是按照webpack3.x的方式,手动在plugins中加入插件。
const webpack = require('webpack')
mode: 'none',
plugins: [
new webpack.optimize.ModuleConcatenationPlugin() // 开启scope hoisting
]
打包之后的代码
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 12);
/******/ })
/************************************************************************/
/******/ ({
/***/ 12:
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
// ESM COMPAT FLAG
__webpack_require__.r(__webpack_exports__);
// CONCATENATED MODULE: ./common/a.js
function testA() {
console.log('this is testA..........');
}
// CONCATENATED MODULE: ./common/b.js
function testB() {
console.log('this is testB..........');
}
testA();
testB();
/***/ })
/******/ });
九、代码分割和动态import
9.1 代码分割的意义
对于大的Web应用来说,将所有的代码都放在同一个文件中显然是不够有效的,比如只打包出一个大大的bundle.js显然是不合理的,特别是当你的某些代码是在某些特定的时候才会被用到。
webpack有一个功能就是将你的代码分割成chunks(语块),当代码运行到需要它们的时候再进行加载。
适用场景:
- 抽离相同代码到一个共享块
- 脚本懒加载,使得初始下载的代码更小。
9.2 懒加载JS脚本
ES6:动态import,目前没得到原生支持,需要babel转换。按需加载,不是让你一开始就把所有模块都加载出来。
npm install @babel/plugin-syntax-dynamic-import --save-dev
打包之后,会把一些包单独打出来,比如本来打包出来的search.js只有这么一个文件,但你你在里面动态加载了一些模块,那么打包之后,就会多出一些文件,也就是会额外的分割一些文件,其实就是从原来的search.js文件里分离出来的。你按了一个按钮,切换一个tab,需要用到某个模块,加载的时机通过你的业务代码去控制,加载到某个模块后就会去请求对应的js,然后去异步的加载这个js,这就是动态import加载的实际效果。
动态import实际举例:
我们在search文件夹下增加search/text.js文件,也就是一个Text组件.
npm install @babel/plugin-syntax-dynamic-import --save-dev
.bablerc文件:
{
"plugins": ["@babel/plugin-syntax-dynamic-import"]
}
打包之后,会把一些包单独打出来,比如本来打包出来的search.js只有这么一个文件,但你你在里面动态加载了一些模块,那么打包之后,就会多出一些文件,也就是会额外的分割一些文件,其实就是从原来的search.js文件里分离出来的。你按了一个按钮,切换一个tab,需要用到某个模块,加载的时机通过你的业务代码去控制,加载到某个模块后就会去请求对应的js,然后去异步的加载这个js,这就是动态import加载的实际效果。
动态import实际举例:
我们在search文件夹下增加search/text.js文件,也就是一个Text组件.
import React from 'react'
export default() => <div>动态引入的import</div>
在search/index.js里面 动态引入这个Text组件。当点击的时候,动态加载这个Text组件。
npm run build之后
我们看看需要动态加载的这个文件的内容:
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[3],{
/***/ 12:
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0);
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
/* harmony default export */ __webpack_exports__["default"] = (function () {
return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", null, "\u52A8\u6001\u5F15\u5165\u7684import");
});
/***/ })
}]);
看到 webpackJsonp 了吗?
点击按钮,加载这个模块的时候,会发起一个jsonp的请求来加载这个模块。
不信,打开Chrome控制台的network。当我们点击,调用加载函数的时候,会出现一个js的请求。
如上图:点击后,懒加载了一个脚本。