前端繁荣发展,工程化已经成为高级前端工程师的必不可少的条件之一,打包构建的发展从grunt
,fis
,glup
到rollup
,webpack
,Parcel
,技术手段变幻无穷,javascript
但其实不论任何一项技术或工具,都有五个阶段,css
这五个阶段越日后是越艰难,可是你越是日后深刻就越能透过表象看清它的本质,以在这快速变化的技术手段中站稳,以不变应万变html
webpack如今是前端打包构建最流行的工具,那么咱们就来好好了解一下它(webpack ^4.42.1)前端
首先梳理下本文要讲到的内容vue
核心概念java
其余经常使用配置node
优化手段react
配置总结webpack
下面逐个介绍css3
定义打包的入口
使用示例
// 简写
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的配置和优化,但咱们仍是要根据项目的复杂程度,公司&项目的具体状况,处理遗留代码的难度,来选择性的处理, 有时若是强行使用反而得不偿失
固然最后要记住一切的技术都只是工具,都要以赋能业务,价值产出为目标,打包工具的目标就是
原本想直接一篇文章直接整理完成的,内容较多,分篇记录,未完待续。。。