webpack:进阶用法(一)
一、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] = entryFilemyHtmlWebpackPlugin.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] = entryFilemyHtmlWebpackPlugin.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] = entryFilemyHtmlWebpackPlugin.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] = entryFilemyHtmlWebpackPlugin.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 cacheif(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 loadedmodule.l = true;// Return the exports of the modulereturn 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的请求。
如上图:点击后,懒加载了一个脚本。
webpack:进阶用法(一)相关推荐
- (数据科学学习手札61)xpath进阶用法
一.简介 xpath作为对网页.对xml文件进行定位的工具,速度快,语法简洁明了,在网络爬虫解析内容的过程中起到很大的作用,除了xpath的基础用法之外(可参考我之前写的(数据科学学习手札50)基于P ...
- Android属性动画进阶用法
2019独角兽企业重金招聘Python工程师标准>>> 在上周二文章中介绍补间动画缺点的时候有提到过,补间动画是只能对View对象进行动画操作的.而属性动画就不再受这个限制,它可以对 ...
- canvas图形处理和进阶用法
前面的话 上一篇博客介绍了canvas基础用法,本文将更进一步,介绍canvas的图形处理和进阶用法 图形变换 图形变换是指用数学方法调整所绘形状的物理属性,其实质是坐标变形.所有的变换都依赖于后台的 ...
- Frida Hook Android App 进阶用法之 Java 运行时
FridaHookAndroid 本文旨在覆盖使用 Frida 对 Android App 进行 hook 的绝大多数场景.文章提到的所有代码以及被测 App,详见:https://github.co ...
- Web scraper使用教程-进阶用法(二)-爬取二级页面内容
进阶用法(二)-爬取二级页面内容 1. 爬取网址 https://docs.microsoft.com/en-us/officeupdates/update-history-microsoft365- ...
- pandas进阶用法(一)筛选条件、多重索引、缺失值
一篇比较好的pandas指南,适合已经熟悉pandas,并想掌握一些进阶用法的读者,不适合对pandas完全不了解的新人.文章大部分是Stack Overflow常见问题集合. pandas 官网 原 ...
- Python Requests库进阶用法——timeouts, retries, hooks
Python HTTP 请求库在所有编程语言中是比较实用的程序.它简单.直观且在 Python 社区中无处不在. 大多数与 HTTP 接口程序使用标准库中的request或 urllib3. 由于简单 ...
- goto在Java中的替代(break和continue的进阶用法)
文章目录 前言 一.goto是什么? 二.goto与Java的关系 1.关系 2.Java中的标签 2.break和continue的进阶用法 总结 前言 goto语句是在源码级上的跳转,这使得其招致 ...
- babel进阶用法之处理json文件
前言 社区里面关于babel的介绍非常多了,这里不想重复这些常见内容.很多人认为babel只是一个语法转译工具,将浏览器无法识别的高级语法进行转换(polyfill),提升开发体验.其实babel是一 ...
最新文章
- ipad无法与itunes同步,提示因为这台电脑不再被授权使用在此ipad上购买的项目解决方案...
- css根据文字长度实现宽度自适应
- c语言中go的作用,go语言与c语言的相互调用
- mysql最大述_mysql最大字段数量及 varchar类型总结
- PAT_B_1004_Java(20分)
- 一位台湾校长的讲话。学习!!!
- 【Python基础入门系列】第09天:Python tuple
- linux下使用John检测用户是否存在弱口令
- TCP/IP之封装,分用,server模型
- Live2D在Unity中的使用
- labuladong算法小抄pdf下载
- navicat的使用技巧
- 大数据分析方法有哪几种?
- php unpack 详解,【PHP】 pack unpack 详解
- Luminar 4:AI 人像照片增强器
- 数字孪生开启传统行业数字化转型升级之路
- 注塑机计算机控制器,注塑机微机控制器,Microprocessor-based Controller for PIM,音标,读音,翻译,英文例句,英语词典...
- Arun Gupta通过将HTML5与Java EE 7拥抱来提高生产力
- DBPwAudit -数据库密码破解工具的使用
- 日常英语单词 - 食物
热门文章
- The Inventor Mentor-第十二章 传感器
- 【集成学习】boosting和bagging
- 【GlobalMapper精品教程】032:浏览地理照片及航线信息(航测应用)
- 软件测试工具大全——全而精
- Unity Shader - Custom DirectionalLight ShadowMap 自定义方向光的ShadowMap
- windows批量删除当前目录下的空文件夹
- 关于EasyNVR拉流摄像头的视频流存在视频流锁定机制的问题说明
- np.digitize()函数
- 浅谈ERP运维在企业信息化建设中的作用
- python可以制作游戏脚本吗_如何用python制作游戏脚本?