前端繁荣发展,工程化已经成为高级前端工程师的必不可少的条件之一,打包构建的发展从grunt
,fis
,glup
到rollup
,webpack
,Parcel
,技术手段变幻无穷,javascript
但其实不论任何一项技术或工具,都有五个阶段,css
这五个阶段越日后是越艰难,可是你越是日后深刻就越能透过表象看清它的本质,以在这快速变化的技术手段中站稳,以不变应万变html
webpack如今是前端打包构建最流行的工具,那么咱们就来好好了解一下它(webpack ^4.42.1)前端
首先梳理下本文要讲到的内容vue
核心概念java
其余经常使用配置node
优化手段react
配置总结webpack
webpack原理css3
loader编写
plugin编写
下面逐个介绍
定义打包的入口
使用示例
// 简写 module.exports = { entry: './src/index.js', } // 多入口 module.exports = { entry: { index: './src/index.js', list: './src/index.js', }, } 复制代码
编译后文件输出到磁盘的相关配置
// 简写 module.exports = { output: { filename: '[name]_[chunkhash:8].js' //单个文件名可直接指定,多入口利用占位符保证文件名统一 path: path.join(__dirname, '../dist') // 写入文件磁盘路径 publicPath: 'http://cdn.example.com/assets/' //资源使用 CDN ,给全部文件引入模版文件时加上路径前缀 }, } 复制代码
占位符
webpack 原生只支持js 和json,利用loader,对不一样文件类型支持,转换成有效的模块 简单示例
module.exports = { module: { rules:[ { test: /\/.txt$/, // 指定匹配规则 use: 'babel-loader' // 指定使用的loader名称 } ] } } 复制代码
下面介绍几种文件类型的处理以及经常使用的loader
babel-loader
:js默认是不支持es6 和jsx语法的,.babelrc
文件: 设置具体支持的属性方法{ test: /\.(j|t)sx?$/, use: 'babel-loader', exclude: /node_modules/ }, 复制代码
style-loader
:将样式经过style
标签插入模版文件的head当中css-loader
: 用于加载.css文件 而且转换成commonjs 对象{ test: /.css$/, use: [ 'style-loader', 'css-loader', ], }, 复制代码
less-loader
:less转换成css,{ test: /.css$/, use: [ 'style-loader', 'css-loader', { loader: 'less-loader', options: { // 可配置属性,修改变量的值,通常利用来修改UI库的主题样式,如下是antd主题样式配置 modifyVars: { '@primary-color': '#ec7259', }, javascriptEnabled: true, }, }, ], }, 复制代码
file-loader
:解析图片, 字体等url-loader
:也可处理图片和字体,,并可设置较小资源自动base64{ test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, use: [ { loader: 'url-loader', options: { limit: 8192, name: 'static/img/[name].[hash:8].[ext]',// [ext] 文件的后缀名 }, }, ], }, { test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, loader: 'url-loader', options: { limit: 8192, name: 'static/fonts/[name].[hash:8].[ext]', }, }, 复制代码
px2rem-loader
: 把px转换成rem,配合lib-flexible使用{ loader: 'px2rem-loader', options: { remUnit: 75, // 1rem=多少像素 remPrecision: 8, // rem的小数点后位数 } } 复制代码
postcss-loader
:用于浏览器适配,某些css3属性浏览器不支持须要加前缀,它会自动针对不一样浏览器加不一样的属性前缀{ loader: 'postcss-loader', options: { plugins: () => [autoprefixer()], }, }, 复制代码
plugins
属性传入new
实例 简单示例const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { plugin: { new webpack.ProgressPlugin(), new HtmlWebpackPlugin({template: './src/index.html'}) } } 复制代码
下面介绍几种经常使用的plugin
new HtmlWebpackPlugin({ filename: '../dist/template/index.html', // 指定生成的模版文件名及路径 template: path.join(__dirname, '../src/template/index.html'), // 指定要使用的模版文件 inject: true, // 指定的chunk会自动注入html文件中 chunks: ['index'], //指定生成的html要使用的chunk minify: { // 代码的最小化输出 collapseWhitespace: true, // 删除空格,可是不会删除SCRIPT、style和textarea中的空格 preserveLineBreaks: false, // 是否保留换行符 minifyCSS: true, // css压缩 minifyJS: true, // js压缩 removeComments: true, // 删除注释,可是会保留script和style中的注释 }, }), 复制代码
rimraf dist
new CleanWebpackPlugin(), 复制代码
new MiniCssExtractPlugin({ filename: '[name]_[contenthash:8].css' }), 复制代码
new OptimizeCssAsssetePlugin({ assetNameRegExp: /\.css$/g, //文件匹配 cssProcessor: require('cssnano') // cssnano 压缩和优化的css插件 }), 复制代码
举例:
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin') plugins: [ new HtmlWebpackExternalsPlugin({ externals:[ { module: 'react', entry: 'https://cdn.cn/16.8.0/react.min.js', global: 'React', }, { module: 'react-dom', entry: 'https://cdn.cn/16.8.0/react-dom.min.js', global: 'ReactDOM', } ] }), 复制代码
development
: 开发模式,production
: 生产模式,none
: 无webpack会针对不一样环境直接作一些优化工做,例如production模式下会进行,tree-shaking
和scope-hosting
下面优化会详细介绍
远古时期,咱们作前端开发时,写一个html文件,在浏览器打开它查看效果,修改时,需手动更新,
后来咱们使用热更新,webpack低版本,不提供热更新的支持,咱们使用插件http-proxy-middleware
和webpack-hot-middleware
,实现热更新,配置比较麻烦
最后webpack把热更新集成在内部,就成了devServer
简单配置以下
devServer: { historyApiFallback: true, // 单页面程序 刷新浏览器会出现404,缘由是它经过这个路径(好比: /search/list)来访问后台,因此会出现404,而把historyApiFallback设置为true那么全部的路径都执行index.html host: '127.0.0.1', // 域名 open: true, //支持自动打开浏览器 hot: true, // 模块热替换,在前端代码变更的时候无需整个刷新页面,只把变化的部分替换掉 inline: false, // inline选项会为入口页面添加“热加载”功能,即代码改变后从新加载页面 port: 8080, // 端口 proxy: proxyConfig._proxy, // 代理后端服务,举例:可本地调试测试接口 before(app) { // 其余中间件以前, 提供执行自定义中间件 apiMocker(app, path.resolve('./mocks/mock.js'), // 举例:可用来作mock数据 proxyConfig._proxy); }, }, 复制代码
首先来看下简单的流程示意图
webpack compile
将JS编译成bundle.jsHMR server
将热更新文件输出给HMR Runtime,HMR -> HotModuleReplacement(热模块替换)Bundle server
提供文件在浏览器的访问HMR Runtime
注入浏览器,更新文件的变化,使浏览器 和 服务器创建一个连接(websocket)bundle.js
构建输出的文件启动阶段
1 -> 2 -> 3
初始代码通过webpack compiler编译进行打包
编译好的文件传输给bundle server, 它就至关于一个服务器,它使文件以server的方式让浏览器访问
files
-> webpack Compiler
-> Bundle Sever
-> bundle.js
更新阶段
1 -> 4 -> 5 -> 6
file文件发生变化,通过webpack compiler编译
编译好的文件传输给HMR server,通知浏览器端的HMR Runtime(一般以JSON形式传输)
HMR Runtime 更新代码,实现无刷新改变页面内容
files
-> webpack Compiler
-> HMR server
-> HMR Runtime
-> code
介绍几个经常使用的属性用法
alias
建立 import 或 require 的别名,来确保模块引入变得更简单extensions
自动解析肯定的扩展mainFileds
当从 npm 包中导入模块时,决定在 package.json 中使用哪一个字段导入模块moudles
告诉 webpack 解析模块时应该搜索的目录举例:
// webpack 配置文件 resolve: { alias: { Util: path.resolve(__dirname, 'src/util/'), }, mainFileds: ['main'], extensions: ['.js', '.jsx', '.json'], moudles: [path.resolve(__dirname, 'node_modules')] }, //业务文件 component.js import Utility from '../../util/utility.js'; // 简化写法(不用写文件路径前缀,也不用写引用文件的扩展名) import Utility from 'Util/utility'; 复制代码
splitChunks
,代替以前的CommonsChunkPlugin,公共资源分离vendors
chunks属性特别说明
splitChunks
公共文件分离commons
runtimeChunk
举例:
optimization: { runtimeChunk: { name: 'manifest', }, splitChunks: { minSize: 50000 // 分离的包的体积大小 cacheGroups: { vendors: { test: /(react|react-dom)/, //正则匹配要分离的文件 name: 'vendors', chunks: 'all', // 肯定对何种引入方式的文件进行分离 minChunks: 1, // 最小使用的次数 priority: 10, // 多个缓存组时,须要有优先级排列,优先使用哪一个进行分离 }, commons: { // 分离公共文件 name: 'commons', chunks: 'all', minChunks: 2, priority: 5, }, }, }, }, 复制代码
关键字定义
eval
模块都使用 eval() 包裹执行,而且都有 //@ sourceURL(指向的是原文件index.js,调试的时候,根据sourceUrl找到的index.js文件的)source map
产生.map文件(这个map文件会和原始文件作一个映射,调试的时候,就是经过这个.map文件去定位原来的代码位置的 )cheap
不包含列的信息,(假如代码运行出现了错误,控制台报出了,error,咱们点击定位到具体源码的时候,就只能定位到行,而不能定位到具体的列)inline
.map文件做为dataUrl嵌入到打包文件,而不单独生成几种关键字进行组合就造成了具体的用法
不一样用法对构建速度是有影响的,基本状况你越清晰容易的看到原始的代码,构建速度就越慢,调试的方便性和构建速度上你们能够本身权衡一下
共有13种用法,详细的请看官方文档
举例
// 开发环境 devtool: 'cheap-module-eval-source-map' // 原始源代码(仅限行) // 生产环境,通常不进行设置 复制代码
构建统计信息
使用举例
// 构建完成后会生成json文件,显示构建的一些信息,时间,各模块的体积等 scripts: { 'build: stats': 'webpack --config build/webpack.prod.config.js --json > stats.json', } 复制代码
const SpeedMeasureWebpackPlugin = require('speed-measure-webpack-plugin') const smp = new SpeedMeasureWebpackPlugin(); module.exports = smp.wrap(merge(webpackConfigBase, webpackConfigProd)); 复制代码
图中咱们看到了每一个插件和loader的耗时状况, 若是耗时较长,会以红字提示,咱们就能够具体分析那个地方为何时间长,能够用别的插件替换之类的去作构建速度优化
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); plugins: [ new BundleAnalyzerPlugin({ analyzerPort: 8919 //打包构建后体积分析图展现的窗口 }), ], 复制代码
运行打包命令,体积分析示意图会自动打开在8919窗口
图中咱们能够看到moment插件占用的空间很大,咱们能够对它进行优化
new webpack.IgnorePlugin(/\.\/locale/, /moment/), // compoent import 'moment/locale/zh-cn'; moment.locale('zh-cn'); 复制代码
Dead Code(什么是死码呢?)
ES6 module 特色:
ES6模块依赖关系是肯定的,和运行时的状态无关,能够进行可靠的静态分析,这就是tree-shaking的基础,让死码清除成为了可能
静态分析就是不执行代码,仅仅从字面的意思上对代码进行分析,ES6以前的模块化,好比咱们能够动态require一个模块,只有执行后才知道引用的什么模块,这个就不能经过静态分析去作优化,特别说明import()
动态引入也是不支持的
引用的文件tools.js
export default 'Hello World'; 复制代码
入口文件index.js
import str from './tools'; console.log(str); 复制代码
未开启scope-hoisting
编译后文件
/* 0 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony import */ var _tools__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); console.log(_tools__WEBPACK_IMPORTED_MODULE_0__["default"]); /***/ }), /* 1 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony default export */ __webpack_exports__["default"] = ('Hello World'); /***/ }) 复制代码
能够看到
开启scope-hoisting
编译后文件
/* 0 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; // ESM COMPAT FLAG __webpack_require__.r(__webpack_exports__); /* harmony default export */ var tools = ('Hello World'); console.log(tools); /***/ }) 复制代码
能够看到
happypack
和thread loader
给咱们提供了方案因为happypack做者再也不维护此项目,同时二者原理大体一致,咱们就主要介绍
thread loader
rules: [ { test: /.js$/, use: [ { loader: 'thread-loader', options: { workers: 3, // 产生的 worker 的数量,默认是 cpu 的核心数 } } ] }, ] 复制代码
注意:thread loader 只有在项目庞大复杂的时候才能显著的凸显效果,若是是中小型项目没有必要使用
日志上报
plugins: [ // 主动捕获构建错误 function () { this.hooks.done.tap('done', (stats) => { if (stats.compilation.errors && process.argv.indexOf('--watch' == -1)) { console.log('error', stats.compilation.errors); // 能够作一个构建系统的日志,在此处上报错误缘由 process.exit(12); // 自定义错误code码 } }); }, ], 复制代码
terser-webpack-plugin
开启 paralleluglify-webpack-plugin
也支持并行压缩,但不支持es6,不作具体介绍,有兴趣的同窗自行查询optimization: { minimizer: [ new TerserPluginWebpack({ parallel: 4, // 开启 不主动指定的话,默认数值是当前电脑cpu数量的2倍减1 }) ], } 复制代码
dll-plugin
进行分包,dllreferenceplugin
对mainfest.json(对分离包的描述)引用,将预编译的模块加载进来add-asset-html-webpack-plugin
把生成文件插入模版举例
// package.json "scripts": { "dll": "webpack --config build/webpack.dll.js" // 打包前只需执行一次,只要基础包不变则不须要执行 }, // webpack.dll.js const path = require('path'); const webpack = require('webpack'); module.exports = { entry: { // 指定要分离的包 library: [ // 基础包 'react', 'react-dom', ], }, output: { filename: "[name]_[hash].dll.js", path: path.resolve(__dirname, './library'), library: "[name]_[hash]", //包名称 注意此名需和下面的DllPlugin,name名一致 }, plugins: [ new webpack.DllPlugin({ name: "[name]_[hash]", path: path.resolve(__dirname, './library/[name].json') }) ] } // webpack.prod.js plugins: [ ... // 将给定的 JS文件添加到 webpack 配置的文件中,并插入到模版文件中 new AddAssetHtmlPlugin({ filepath: path.resolve(__dirname, '../build/library/*.dll.js'), }), // new webpack.DllReferencePlugin({ manifest: require('../build/library/library.json') }), ] 复制代码
hard-source-webpack-plugin
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin'); module.exports = { plugins: [ ... new HardSourceWebpackPlugin() ] ]} 复制代码
总结
vue-cli
和create-react-app
中并无使用dllhard-source-webpack-plugin
替换dll
举例
const PATHS = { src: path.join(__dirname, '../src') } plugin: [ new PurgecssPlugin({ paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }), // 注意是绝对路径匹配 }), ] 复制代码
{ test: /.(png|jpg|gif|jpeg)$/, use: [ { loader: 'url-loader', options: { limit: 8192, name:'[name]_[hash:8].[ext]' } }, { loader: 'image-webpack-loader', options: { mozjpeg: { progressive: true, quality: 65 }, optipng: { enabled: false, }, pngquant: { quality: [0.65, 0.90], speed: 4 }, gifsicle: { interlaced: false, }, webp: { quality: 75 } } }, ], }, 复制代码
https://polyfill.io/v3/polyfill.min.js
https://polyfill.alicdn.com/polyfill.min.js?features=Promise
咱们以实际目标为导向收集整理一下经常使用webpack配置要作什么事情
小结
虽说了不少关于webpack的配置和优化,但咱们仍是要根据项目的复杂程度,公司&项目的具体状况,处理遗留代码的难度,来选择性的处理, 有时若是强行使用反而得不偿失
固然最后要记住一切的技术都只是工具,都要以赋能业务,价值产出为目标,打包工具的目标就是
关于webpack原理
和loader&plugin
请看 webpack4配置到优化到原理(下)