[前端开发]Webpack3/4配置整理

webpack3配置

npm install webpack@3 -g #安装最新的3.x版本的webpack
npm install webpack-cli@2 -g 

命令行下webpack常见配置参数

  • --config 设置配置文件
  • -p 打包压缩
    --progress 打印打包进度
  • -watch, -w 监听文件变化
  • --entry 指定入口文件
  • --hot 开启热更新
  • webpack entry output 指定入口文件和打包后的文件

1.0 编译ES6配置

  "devDependencies": {
    "@babel/core": "^7.6.2",
    "@babel/plugin-transform-runtime": "^7.6.2",
    "@babel/preset-env": "^7.6.2",
    "@babel/runtime": "^7.6.2",
    "babel-loader": "^8.0.0-beta.0",
    "babel-polyfill": "^6.26.0"
  }

使用webpack配置文件方式编译ES6

import "babel-polyfill"
...
    module:{
        rules:[{
            test:/\.js$/,
            exclude:"/node_modules/",
            use:{
                loader:"babel-loader",
                options:{
                    presets:[
                        ["@babel/preset-env",{
                            targets:{
                                browsers:["last 2 versions","> 1%"]
                            }
                        }]
                    ]
                }
            }
        }]
    }

使用babel配置文件的方式

{
    "presets":[["@babel/preset-env",{
        "targets":{
            broswers:["last 2 versions","> 1%"]
        }
    }]],
    "plugins":["@babel/transform-runtime"]
}

想请教一下,既然babel-runtime和plugin-transform-runtime也可以使项目支持ES6新增API,而且压缩文件那么小,那生产环境中是否只用babel-polyfill就可以啦呢?我之前看的视频说babel-polyfill在生产环境下用,而runtime用在一个框架库上,这是为何?

1.2 编译typescript

  • 安装依赖
npm install typescript ts-loader awesome-typescript -D
  • 创建tsconfig.json
{
    "compilerOption":{
        "module":"commonjs",
        "target":"es5",
        "allowJs":true,
        "typeRoot":[
            "./node_modules/@types",  
            "./typings/modules" 
        ]
    },
    "include":["./src/*"],
    "exclude":["./node_modules"]
}
  • 使用库文件的声明文件
1.npm install @types/ts写的库
2.npm install typing && typing install ts写的库文件
  **会在项目目录生成typings的文件夹,里面是库文件的声明文件**

1.3 公共代码提取

pageA,B分别引入subPageA,B,而subPageA,B又同时引入module.js文件和第三方库lodash.CommonschunksPlugins只适用于多entry的情况,所以需要多的entry

const  path = require("path");
const webpack = require("webpack");
module.exports = {
    entry:{
        PageA:"./src/CommonChunk/pageA.js",
        PageB:"./src/CommonChunk/pageB.js",
        vendor:["lodash"]
    },
    output:{
        path:path.resolve(__dirname,"./dist/"),
        filename:"[name].bundle.js",
        chunkFilename:"[name].chunk.js"//非入口(non-entry) chunk 文件的名称
    },
    plugins:[
            // //chunks指定提取的公共部分的范围,否则如果下面的lodash提取的时候(infinity)报错
            new webpack.optimize.CommonsChunkPlugin({
                name:"common",
                minChunks:2,//当大于等于minChunks设定的值时,该模块就会被打包到公用包中
                chunks:["PageA","PageB"]//指定它提取代码的范围
            }),
            //会将lodash用webpack打包提取出来,里面包含webpack的代码和lodash的代码
            // new webpack.optimize.CommonsChunkPlugin({
            //  names:["vender"]
            //  minChunks:Infinity
            // }),
             // 会将loadash和webpack自己的代码打包到vender-chunk文件中,但是不包括pageA和pageB的公共部分
            new webpack.optimize.CommonsChunkPlugin({
               //相当于将第二次的非lodash代码单独提取到manifest.bundle.js文件中
                names:["vendor","mainfest"],
                minChunks:Infinity
            }),
    ]
}

1.4 代码分割&懒加载

懒加载require.ensure

const  path = require("path");
const webpack = require("webpack");
module.exports = {
    entry:{
        PageA:"./src/CommonChunk/pageA.js",
         PageB:"./src/CommonChunk/pageB.js",
         vendor:["lodash"]
    },
    output:{
        path:path.resolve(__dirname,"./dist/"),
        filename:"[name].bundle.js",
        publicPath:"./dist/",
        chunkFilename:"[name].chunk.js"
    },
  plugins[ 
       new webpack.optimize.CommonsChunkPlugin({
            //会将pageA/B的懒加载公共部分单独提取到async-common.chunk.js中
              async:"async-common",
              children:true,//就不需要chunks配置
              minChunks:2//当大于等于minChunks设定的值时,该模块就会被打包到公用包中
        }),
        new webpack.optimize.CommonsChunkPlugin({
              name:"common",
              minChunks:2,//当大于等于minChunks设定的值时,该模块就会被打包到公用包中
              chunks:["PageA","PageB"]//指定它提取代码的范围
        })
]
}
import "./moduleA";
export  function subPageA(a,b){
    return a+b;
}
// 对pageAB进行懒加载,只有需要的的时候才require
 require.ensure(["./subPageA"],function(){
    var A = require("./subPageA");
        A.subPageA(11,33)
 },"subPageA");#会生成subPageA.chunk.js的异步提取文件

懒加载之import
package.json和.babelrc需要额外安装才能支持import动态加载

"babel-plugin-syntax-dynamic-import": "^6.18.0"
"plugins":["syntax-dynamic-import"]
//pageA
import(/* webpackChunkName:'subA' */"./subPageA")
        .then(function(subA){
            console.log("subA=====>",subA)
        });

1.5 样式处理

{
  "scripts": {
    "build": "webpack -p --progress  --config webpack.conf.js"
  },
  "dependencies": {
    "css-loader": "^1.0.0",
    "cssnano": "^4.1.4",
    "extract-text-webpack-plugin": "^3.0.2",
    "lodash": "^4.17.11",
    "postcss": "^7.0.5",
    "autoprefixer": "^9.1.5",
    "postcss-cssnext": "^3.1.0",
    "postcss-loader": "^3.0.0",
    "purify-css": "^1.2.5",
    "sass-loader": "^7.1.0",
    "style-loader": "^0.23.0"
  },
  "devDependencies": {
    "glob-all": "^3.1.0",
    "node-sass": "^4.12.0",
    "purifycss-webpack": "^0.7.0",
    "webpack": "^3.5.6"
  },
//让css和js都使用该配置统一浏览器
  "browserslist": [
    "last 2 versions",
    ">= 1%"
  ]
}
const  path = require("path");
const webpack = require("webpack");
const purifyCSS = require("purifycss-webpack");
// 处理多路径的css,比如说js,html中的
const glob = require("glob-all");
const ExtractTextWebpackPlugin = require("extract-text-webpack-plugin");
module.exports = {
    module:{
        rules:[{
            test:/\.scss$/,
            use:ExtractTextWebpackPlugin.extract({
                fallback:{
                    loader:"style-loader",
                    options:{
                        // insertInto:"#Box",
                        // 使插入到app节点的样式显示为一个style标签
                        singleton:true,
                        transform:"./src/css.transform.js"
                    }
                },
                use:[{
                        loader:"css-loader",
                        options:{
                            // minimize:false,//已经被其他替换,这里不起作用,请看github issue
                            modules:true,
                            localIdentName:"[path][name]_[local]--[hash:base64:3]"
                        }
                    },{
                        loader:"postcss-loader",
                        options:{
                            ident:"postcss",
                            plugins:[
                                // require("autoprefixer")(),//cssnext已经存在
                                require("postcss-cssnext")()
                            ]
                        }
                    },{
                        loader:"sass-loader"
                    }
                ]})
        }]
    },
    plugins:[
        new ExtractTextWebpackPlugin({
            filename:"[name].min.css",
            allChunks:false//只提取初始化的样式,而不是异步加载的样式(ensure,import)
        }),
        // new purifyCSS({
        //  paths:glob.sync([
        //      // 针对html,js里面添加的class进行全局查找,csss用上的就提取,没用上的不提取
        //      
        //      path.join(__dirname,"./*.html"),
        //      path.join(__dirname,"./src/**/*.js")
        //  ])
        // })
    ]
}

css tree shaking一定要在extract之后,但是开启css-loader module之后purifyCSS就有点问题

PurifyCSS doesn't support classes that have been namespaced with CSS Modules. However, by adding a static string to css-loader's localIdentName, you can effectively whitelist these namespaced classes.

https://github.com/webpack-contrib/purifycss-webpack

JS tree shaking 使用

new webpack.optimize.UglifyJsPlugin({
    compress: {
        warnings: false,
        drop_console: true
        ...
    },
})

1.6打包其他资源文件

  • file-loader处理图片编译
  • url-loader图片base64处理
  • img-loader图片压缩
  • postcss-sprites生成精灵图层

1.7 第三方资源引用配置

resolve:{
    alias:{
       #针对自定义的第三方库路径
        lodash$:path.resolve(__dirname,"./lib/lodash/lodash.min.js")
    }
},
plugins:[
        new webpack.ProvidePlugin({
           #key:自定义变量,val:指向node_modules中的模块名
             $:"jquery",
              _:"lodash"
        })
    ]

1.8 自动生成HTML

const HtmlWebpackPlugin = require("html-webpack-plugin");
const HtmlInlineChunkPlugin = require("html-inline-chunk-plugin");

...
entry:{
      app:"./src/app.js",
      main:"./main.js"
      vendor:["jquery"]
  },
plugins:[
       new webpack.optimize.CommonsChunkPlugin({
            name: ['vendor','runtime'],
            filename: '[name].js',
            minChunks: Infinity
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'common',
            filename: '[name].js',
            chunks: ['app','main']#抽取commons chunk
        }),
        #用于将webpack运行时的公共代码直接插入到html页面,防止多余的http请求产生
        new HtmlInlineChunkPlugin({
             inlineChunks:["runtime"]
        }),
        new HtmlWebpackPlugin ({
                filename:"index.html",
                template:"./index.html",
                inject:true,#将打包生成的css和js插入到html中
                chunks:["app"]#针对多entry的只插入指定的entry chunks
                minify:{
                    collapseWhitespace:true
                }
        })
    ]

1.9 模块热更新配置

  • historyApiFallback:单页面应用网地址栏输入路由的时候会自动跳转 到index.html,否则会报404
  • port 端口
  • hot 热更新配置
  • overlay: true,出现错误之后会在页面中出现遮罩层提示
  • inline 在页面显示出更新信息,默认为true,不显示更新信息
  • proxy http代理
    • target
    • changeOrigin
    • header设置代理的请求头
    • pathRewrite
#当前是在127.0.0.1:4000端口
# $.get("http://localhost:4000/api/blog")
devServer:{
  port :3000,
  proxy:{
      "/":{
            target:"http:www.myblog.com",
            changeOrigin:true,
            logLevel:"debug"#会在终端输出代理信息,
            headers:{
                  "cookie":"..." #使用该用户信息登录
            },
            pathRewrite:{
                    "^/blog":"/api/blog" #
            }  
      }
   }
}
# pathRewrite的结果" localhost:4000/blog=>  localhost:4000/api/blog=>http:www.myblog.com/api/blog

在plugins中加入以下插件即可,而css是通过style-loader实现热更新,所以如果是通过提取的方式是无法实现热更新

new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(), 

#在js代码主入口文件中需要加入
if(module.hot){
  module.hot.accept(moduleId, callback);
}

但是一般的react和vue的loader都会帮我们处理js代码中的上述步骤

2.0 开启开发调试

css开发调试需要在css的每个loader中加上sourceMap:true属性

devtool:"cheap-module-surce-map"
...

new webpack.optimize.UglifyJsPlugin({sourceMap: true})

2.1 区分生产环境和开发环境

开发环境需要的内容

  • 代码热更新
  • sourceMap(devtool)
  • 代码规范检查(ESLint)
  • 服务器器代理(webpack-dev-server)

生产环境需要的内容

  • 公用代码提取(extract-text-webpack-pluginpurifycss-webpack)
  • 去除无用代码(Tree shaking)
  • 文件压缩(img-loader,url-loader)
  • 压缩混淆(webpack.optimize.UglifyJsPlugin)

2.2 开发和生产环境配置

create-react-app配置热更新

  • 安装react-hot-loader
cnpm i react-hot-loader -D
  • webpack加入配置
entry: [
     #放在babel-polyfill之后,其他项之前
    "react-hot-loader/patch"
],
devServer: {
     hot: true,
     ...
},
plugins:[
    new webpack.hotModuleReplacementPlugin()
    ...
]

  • 在.babelrc中添加plugin
"plugins": ["react-hot-loader/babel"]

  • 入口文件配置
import React from "react";
import ReactDOM from "react-dom";
import { AppContainer } from "react-hot-loader"
import App from "./components/App/";

const render = Component => {
    ReactDOM.render(
        
            
        ,
         document.getElementById("root")
    );
}

render(App);

if(module.hot){
    module.hot.accept("./components/App/",()=>{
          render(App)
    })
}

自己搭建react HMR环境

https://segmentfault.com/a/1190000011151106

Webpack4 打包配置

1.0 HTML资源内联

${require(raw-loader!babel-loader+资源的路径)}

1.1 资源压缩

在mode为production的情况下js默认打包压缩,如果还需要其他压缩我配置需要安装uglifyjs-webpack-plugin

HTML压缩和webpack3一致

  new HtmlWebpackPlugin({
           template:path.join(__dirname,"./index.html"),
           inject:true,
           filename:"index.html",
           minify:{
             removeComments:true,
             collapseWhitespace:true
           }
       }),

css压缩配置

const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
   new OptimizeCSSAssetsPlugin({
            assetNameRegExp: /\.css\.*(?!.*map)/g,  //注意不要写成 /\.css$/g
            cssProcessor: require('cssnano'),
            cssProcessorOptions: {
                discardComments: { removeAll: true },
                // 避免 cssnano 重新计算 z-index
                safe: true,
                // cssnano 集成了autoprefixer的功能
                // 会使用到autoprefixer进行无关前缀的清理
                // 关闭autoprefixer功能
                // 使用postcss的autoprefixer功能
                autoprefixer: false
            },
            canPrint: true
        }),

1.2 公共提取

(待更新...)

1.3 Tree Shaking

JS Tree Shaking
webpack4下JS TreeShaking 默认打开,前提是js代码是ES6语法写的,方法没有副作用(没有引用到全局或者函数外部的变量,单一的输入有单一的输出),同时mode为production

CSS Tree Shaking

#webpack3
 new purifyCSS({
    paths:glob.sync([
              # 针对html,js里面添加的class进行全局查找,csss用上的就提取,没用上的不提取      
             path.join(__dirname,"./*.html"),
             path.join(__dirname,"./src/**/*.js")
     ])
  });
#webpack4
new purgecss-webpack-plugin({
     #内容同上,但是需要配合mini-css-extract-plugin一起使用
})

1.4 Scope Hoisting

原理:将所有代码按照模块引用顺序放在一个函数作用域里面,然后还适当的重命名一些变量,防止变量名冲突。通过Scope Hoisting可以减少函数声明和内存开销(因为没有模块都是以闭包形式的包裹)。
开启Scope Hoisting的方式mode="production"或者手动引入webpack.optimize.ModuleConcatenationPlugin,该插件是在production自动会引入的,production情况下有很多其他插件都是默认打开的

Sets process.env.NODE_ENV on DefinePlugin to value production . Enables FlagDependencyUsagePlugin , FlagIncludedChunksPlugin , ModuleConcatenationPlugin , NoEmitOnErrorsPlugin , OccurrenceOrderPlugin , SideEffectsFlagPlugin and TerserPlugin .

1.5 webpack4 集成ESLint代码检查规范

  • 安装依赖
npm install eslint  
eslint-plugin-import
eslint-plugin-react  
eslint-plugin-react-hooks 
eslint-plugin-jsx-a11y 
eslint-loader 
babel-eslint(eslint解析器) -D
  • 在webpack解析JS的babel-loader后面加入eslint-loader
  • 创建eslint配置文件
module.exports = {
  "parser":"babel-eslint",
  "exnteds":"airbnb",
  "env":{
    "brower":true,
    "node":true
  },
  rules:{
      ...  
  }
}

EsLint官方指南
eslint-config-airbnb

1.6 打包自己的组件和库

  • 创建webpack.prod.js文件配置

const TerserPlugin = require('terser-webpack-plugin') // 引入压缩插件
 
module.exports = {
  entry: {
    'myLibrary': './src/index.js',
    'myLibrary.min': './src/index.js'
  },
  output: {
    filename: '[name].js',
    library: 'myLibrary',#对外输出的模块名,指定库的全局变量
    libraryTarget: 'umd',
    libraryExport: 'default'   #可以直接 new myLibrary(),而不是new myLibrary().default;
  },
  mode: 'none', // 设置mode为none避免默认压缩
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({ // 使用压缩插件,只压缩min结尾的
        include: /\.min\.js$/
      })
    ]
  }
}
  • 编写自己库的代码
  • 根据package.json中main字段指定的文件定义不同开发环境下引用的不同库打包文件
if (process.env.NODE_ENV === 'production') { // 通过环境变量来决定入口文件
  module.exports = require('./dist/myLibrary.min.js')
} else {
  module.exports = require('./dist/myLibrary.js')
}

1.7 发布库或组件到npm

  • 注册npm账户
  • 终端输入 npm login
  • 输入注册的账户&密码
  • npm publish

npm包升级

  • npm version patch =>#v1.0.1
  • npm version minor =>#v1.1.0
  • npm version major =>#v2.0.0

1.8 服务端渲染(SSR)打包配置

(带更新...)

1.9 构建日志的处理

npm install friendly-errors-webpack-plugins -D
#webpack.dev.config.js
devServer:{stats:"errors-only"}
#webpack.prod.config.js
#直接添加一个一段
stats:"errors-only"

2.0 单元测试(mocha & jest)

By default, mocha looks for the glob "./test/*.js", so you may want to put your tests in test/ folder. If you want to include subdirectories, pass the --recursive option.
To configure where mocha looks for tests, you may pass your own glob:

mocha --recursive "./spec/*.js"或者
mocha "./spec/**/*.js"

Mocha
需要测试方法函数

#mocha/demoCase/demo.js
module.exports = function (...rest) {
    var sum = 0;
    for (let n of rest) {
        sum += n;
    }
    return sum;
};

每个it("name", function() {...})就代表一个测试

#mocah/demoCase/test/demo-test.js
const assert = require('assert');
const sum = require('../demoCase/demo');

describe('mode.js', () => {
    describe('#sum()', () => {
        it('sum() should return 0', () => {
            assert.strictEqual(sum(), -1);
        });
        it('sum(1) should return 1', () => {
            assert.strictEqual(sum(1), 1);
        });
    });
});
#package.json
"script":{
  "test":"mocha"
}

测试覆盖率
测试的时候,我们常常关心,是否所有代码都测试到了。
这个指标就叫做"代码覆盖率"(code coverage),它有四个测量维度。

  • 行覆盖率:是否每一行都执行了?
  • 函数覆盖率:是否每个函数都调用了?
  • 分支覆盖率:是否每个if代码块都执行了?
  • 语句覆盖率:是否每个语句都执行了?
npm install istanbul -D 
#package.json
"test":"istanbul cover ./node_modules/bin/_mocha"

https://github.com/gotwarlost/istanbul
阮一峰:代码覆盖率工具 Istanbul 入门教程
Jest
(待更新...)

2.1 持续集成Travis CI

持续集成服务 Travis CI 教程-阮一峰
how-to-use-travis-ci
Building a JavaScript and Node.js project in travis CI

2.2 错误处理与捕获

#在plugin中添加一个错误捕获插件
 function() {
    this.hooks.done.tap('done', (stats) => {
        if (stats.compilation.errors && stats.compilation.errors.length && process.argv.indexOf('--watch') == -1) {
                  console.log('build error');
                  process.exit(1);
              }
         })
     }

2.3 打包性能分析

webpack可视化打包性能分析插件

2.4 打包优化方案

  • 预编译资源模块 DllPlugin 和DllReferencePlugin
#webpack.dll.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
    entry: {
        reactLib: [
            'react',
            'react-dom'
        ]
    },
    output: {
        filename: '[name]_[chunkhash:4].dll.js',
        path: path.join(__dirname, 'dll/lib'),
        library: '[name]_dll' #var reactLib_dll=function(e){var t={}...
    },
    plugins: [
        new webpack.DllPlugin({
            name: '[name]_dll',#同上library值
            path: path.join(__dirname, 'dll/lib/[name].json')
        })
    ]
};

npm run build:dll之后会生成reactLib.json和[name]_[chunkhash:4].dll.js文件

"build:dll":"cross-env NODE_ENV=production webpack -p --config webpack.dll.config.js"
# webpack.prod.config.js
 new webpack.DllReferencePlugin({
      manifest: require('./build/library/library.json')
})

在prod文件plugin字段中添加引用,执行npm run build即可,npm run build:dll只需要执行一遍就行

  • 缩小构建文件查找的范围
    • 置loader时,通过test、exclude、include缩小搜索范围
    • 合理使用resolve字段
      • 设置resolve.modules:[path.resolve(__dirname, 'node_modules')]避免层层查找
      • 设置resolve.mainFields:['main'],设置尽量少的值可以减少入口文件的搜索步骤
  • 开启构建缓存,提升构建速度
    • hard-source-webpack-plugin
    • 设置tester-webpack-plugin cache为true
    • babel-loader?cacheDirectory=true
  • 多进程多实例并行构建
    • happypack webpack3 解决方案
const HappyPack = require('happypack');

exports.module = {
  rules: [
    {
      test: /.js$/,
      // 1) replace your original list of loaders with "happypack/loader":
      // loaders: [ 'babel-loader?presets[]=es2015' ],
      use: 'happypack/loader',
      include: [ /* ... */ ],
      exclude: [ /* ... */ ]
    }
  ]
};

exports.plugins = [
  // 2) create the plugin:
  new HappyPack({
    // 3) re-add the loaders you replaced above in #1:
    loaders: [ 'babel-loader?presets[]=es2015' ]
  })
];
  • thread-loader webpack4解决方案

  • 多进程多实例并行压缩

    • new webpack.optimize.UglifyJsPlugin webpack3解决方案,设置parallel和cache为true并且关闭生产环境下的sourceMap
    • webapck-parallel-webpack-plugin 默认开启并行压缩
    • uglify-webpack-plugin 设置parallel为并行数
    • tester-webpack-plugin 设置parallel为某并行数
  • 长缓存优化

    • 提取vendor
    • hash->chunkHash
    • 提取webpack runtime
    • 异步加载的模块自定义chunkName
  • chunk: 是指代码中引用的文件(如:js、css、图片等)会根据配置合并为一个或多个包,我们称一个包为 chunk。
  • module: 是指将代码按照功能拆分,分解成离散功能块。拆分后的代码块就叫做 module。可以简单的理解为一个 export/import 就是一个 module。

在我们引入其他模块的时候模块顺序变化,vendor hash也会变化,解决方案:new NamedModulesPlugin 固定moduleID
而在动态加载的模块过程中也会发生vendor hash改变的情况,解决方案:new NamedChunksPlugin 固定chunkID

import(/* webpackChunkName: "my-chunk-name" */ "module");

基于webpack4[.3+]构建可预测的持久化缓存方案

对于lodash这种第三方库,正确的用法是只去import所需的函数(用什么引什么),例如:

// 正确用法`
import isPlainObject from   'lodash/isPlainObject'
//错误用法`
import { isPlainObject } from 'lodash'

这两种写法的差别在于,打包时webpack会根据引用去打包依赖的内容,所以第一种写法,webpack只会打包lodash的isPlainObject库,第二种写法却会打包整个lodash。现在假设在项目中只是用到不同模块对lodash里的某几个函数并且没有对于某个函数重复使用非常多次,那么这时候把lodash添加到dll中,带来的收益就并不明显,反而导

动态polyfill

https://c7sky.com/polyfill-io.html

webapck面试集

https://cloud.tencent.com/developer/article/1356611

你可能感兴趣的:([前端开发]Webpack3/4配置整理)