├── static ├── .gitkeep └── webpack.png ├── .gitignore ├── .babelrc ├── src ├── assets │ └── webpack.png ├── page.js ├── index.js ├── page.scss ├── index.html ├── page.html └── index.scss ├── .postcssrc.js ├── package.json └── README.md /static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "presets": [["env", {"loose": true, "modules": false}], "stage-0"] 4 | } -------------------------------------------------------------------------------- /static/webpack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wlx200510/webpack4.x-learn/HEAD/static/webpack.png -------------------------------------------------------------------------------- /src/assets/webpack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wlx200510/webpack4.x-learn/HEAD/src/assets/webpack.png -------------------------------------------------------------------------------- /src/page.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | // import './page.scss' 3 | require('./page.scss') 4 | import $ from 'jquery' 5 | import _ from 'lodash' 6 | window.onload = function() { 7 | var text = $('#text') 8 | console.log(text.html) 9 | text.text = _.upperCase(text.text) 10 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | require('./index.scss') 2 | import $ from 'jquery' 3 | import _ from 'lodash' 4 | window.onload = function() { 5 | console.log(process.env.NODE_ENV) 6 | var text = document.getElementsByClassName('content-div') 7 | console.log(text.length) 8 | console.log(text[0].innerHTML, '1') 9 | var contentTest = _.repeat('karl ', 2) 10 | console.log(contentTest) 11 | } -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "plugins": { 3 | // to edit target browsers: use "browserslist" field in package.json 4 | "autoprefixer": { 5 | "browsers": [ 6 | "ie >= 9", 7 | "ff >= 30", 8 | "chrome >= 34", 9 | "safari >= 7", 10 | "opera >= 23" 11 | ] 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/page.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: red; 3 | .example{ 4 | color: #ffffff; 5 | font-size: 20px; 6 | } 7 | .my-name{ 8 | width: 100%; 9 | height: 100px; 10 | span { 11 | font-size: 18px; 12 | color: brown; 13 | text-align: center; 14 | display: inline-block; 15 | width: 100%; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
Hello World 2
11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | My Name is Karl 12 |
13 |
14 | EXAMPLE_PAGE 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: blue; 3 | .content-div{ 4 | color: #ffffff; 5 | font-size: 20px; 6 | } 7 | /* 下面用于css的treeShaking是不是管用*/ 8 | .useless{ 9 | background-color: #000000; 10 | color: #ff0000; 11 | } 12 | /* 下面用于css的路径转换是不是管用*/ 13 | .test-image{ 14 | background-image: url('./assets/webpack.png'); 15 | background-repeat: no-repeat; 16 | background-size: 300px; 17 | width: 300px; 18 | height: 300px; 19 | margin: 0 auto; 20 | } 21 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack4-learn", 3 | "sideEffects": [ 4 | "*.css", 5 | "node_modules/**/*" 6 | ], 7 | "version": "0.0.1", 8 | "description": "learn webpack4 config change", 9 | "main": "index.js", 10 | "scripts": { 11 | "build": "cross-env NODE_ENV='production' webpack --config build/webpack.prod.config.js --mode production", 12 | "dev": "cross-env NODE_ENV='development' webpack-dev-server --open --config build/webpack.dev.config.js --mode development", 13 | "dll": "webpack --config build/webpack.dll.config.js --mode production", 14 | "start": "npm run dll && npm run dev", 15 | "prod": "npm run dll && npm run build" 16 | }, 17 | "keywords": [ 18 | "webpack4", 19 | "cli" 20 | ], 21 | "author": "wlx200510", 22 | "license": "ISC", 23 | "devDependencies": { 24 | "babel-core": "^6.26.0", 25 | "babel-loader": "^7.1.4", 26 | "babel-preset-env": "^1.6.1", 27 | "babel-preset-stage-0": "^6.24.1", 28 | "clean-webpack-plugin": "^0.1.19", 29 | "copy-webpack-plugin": "^4.5.1", 30 | "cross-env": "^5.1.6", 31 | "css-hot-loader": "^1.3.9", 32 | "css-loader": "^0.28.11", 33 | "file-loader": "^1.1.11", 34 | "happypack": "^5.0.0", 35 | "html-webpack-plugin": "^3.2.0", 36 | "mini-css-extract-plugin": "^0.4.0", 37 | "node-sass": "^4.8.3", 38 | "optimize-css-assets-webpack-plugin": "^4.0.0", 39 | "postcss-loader": "^2.1.4", 40 | "progress-bar-webpack-plugin": "^1.11.0", 41 | "purify-css": "^1.2.5", 42 | "purifycss-webpack": "^0.7.0", 43 | "sass-loader": "^7.0.1", 44 | "style-loader": "^0.21.0", 45 | "url-loader": "^1.0.1", 46 | "webpack": "^4.6.0", 47 | "webpack-cli": "^2.0.15", 48 | "webpack-dev-server": "^3.1.3", 49 | "webpack-merge": "^4.1.2", 50 | "webpack-parallel-uglify-plugin": "^1.1.0" 51 | }, 52 | "dependencies": { 53 | "jquery": "^3.5.0", 54 | "lodash": "^4.17.13" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](./static/webpack.png) 2 | 3 | ## 学习webpack4的配置更改 4 | 5 | > webpack作为一个模块打包器,主要用于前端工程中的依赖梳理和模块打包,将我们开发的具有高可读性和可维护性的代码文件打包成浏览器可以识别并正常运行的压缩代码,主要包括样式文件处理成`css`,各种新式的`JavaScript`转换成浏览器认识的写法等,也是前端工程师进阶的不二法门。 6 | 7 | ### webpack.config.js配置项简介 8 | 9 | 1. Entry:入口文件配置,Webpack 执行构建的第一步将从 Entry 开始,完成整个工程的打包。 10 | 2. Module:模块,在`Webpack`里一切皆模块,`Webpack`会从配置的`Entry`开始递归找出所有依赖的模块,最常用的是`rules`配置项,功能是匹配对应的后缀,从而针对代码文件完成格式转换和压缩合并等指定的操作。 11 | 3. Loader:模块转换器,用于把模块原内容按照需求转换成新内容,这个是配合`Module`模块中的`rules`中的配置项来使用。 12 | 4. Plugins:扩展插件,在`Webpack`构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要的事情。(插件`API`) 13 | 5. Output:输出结果,在`Webpack`经过一系列处理并得出最终想要的代码后输出结果,配置项用于指定输出文件夹,默认是`./dist`。 14 | 6. DevServer:用于配置开发过程中使用的本机服务器配置,属于`webpack-dev-server`这个插件的配置项。 15 | 16 | ### webpack打包流程简介 17 | 18 | - 根据传入的参数模式(`development` | `production`)来加载对应的默认配置 19 | - 在`entry`里配置的`module`开始递归解析`entry`所依赖的所有`module` 20 | - 每一个`module`都会根据`rules`的配置项去寻找用到的`loader`,接受所配置的`loader`的处理 21 | - 以`entry`中的配置对象为分组,每一个配置入口和其对应的依赖文件最后组成一个代码块文件(chunk)并输出 22 | - 整个流程中`webpack`会在恰当的时机执行`plugin`的逻辑,来完成自定义的插件逻辑 23 | 24 | ### 基本的webpack配置搭建 25 | 26 | 首先通过以下的脚本命令来建立初始化文件: 27 | 28 | ```bash 29 | npm init -y 30 | npm i webpack webpack-cli -D // 针对webpack4的安装 31 | mkdir src && cd src && touch index.html index.js 32 | cd ../ && mkdir dist && mkdir static 33 | touch webpack.config.js 34 | npm i webpack-dev-server --save-dev 35 | ``` 36 | 37 | 修改生成的`package.json`文件,来引入`webpack`打包命令: 38 | 39 | ```json 40 | "scripts": { 41 | "build": "webpack --mode production", 42 | "dev": "webpack-dev-server --open --mode development" 43 | } 44 | ``` 45 | 46 | 对`webpack.config.js`文件加入一些基本配置`loader`,从而基本的`webpack4.x`的配置成型(以两个页面入口为例): 47 | 48 | ```javascript 49 | const path = require('path'); 50 | const CopyWebpackPlugin = require('copy-webpack-plugin') // 复制静态资源的插件 51 | const CleanWebpackPlugin = require('clean-webpack-plugin') // 清空打包目录的插件 52 | const HtmlWebpackPlugin = require('html-webpack-plugin') // 生成html的插件 53 | const ExtractTextWebapckPlugin = require('extract-text-webpack-plugin') //CSS文件单独提取出来 54 | const webpack = require('webpack') 55 | 56 | module.exports = { 57 | entry: { 58 | index: path.resolve(__dirname, 'src', 'index.js'), 59 | page: path.resolve(__dirname, 'src', 'page.js'), 60 | vendor:'lodash' // 多个页面所需的公共库文件,防止重复打包带入 61 | }, 62 | output:{ 63 | publicPath: '/', //这里要放的是静态资源CDN的地址 64 | path: path.resolve(__dirname,'dist'), 65 | filename:'[name].[hash].js' 66 | }, 67 | resolve:{ 68 | extensions: [".js",".css",".json"], 69 | alias: {} //配置别名可以加快webpack查找模块的速度 70 | }, 71 | module: { 72 | // 多个loader是有顺序要求的,从右往左写,因为转换的时候是从右往左转换的 73 | rules:[ 74 | { 75 | test: /\.css$/, 76 | use: ExtractTextWebapckPlugin.extract({ 77 | fallback: 'style-loader', 78 | use: ['css-loader', 'postcss-loader'] // 不再需要style-loader放到html文件内 79 | }), 80 | include: path.join(__dirname, 'src'), //限制范围,提高打包速度 81 | exclude: /node_modules/ 82 | }, 83 | { 84 | test:/\.less$/, 85 | use: ExtractTextWebapckPlugin.extract({ 86 | fallback: 'style-loader', 87 | use: ['css-loader', 'postcss-loader', 'less-loader'] 88 | }), 89 | include: path.join(__dirname, 'src'), 90 | exclude: /node_modules/ 91 | }, 92 | { 93 | test:/\.scss$/, 94 | use: ExtractTextWebapckPlugin.extract({ 95 | fallback: 'style-loader', 96 | use:['css-loader', 'postcss-loader', 'sass-loader'] 97 | }), 98 | include: path.join(__dirname, 'src'), 99 | exclude: /node_modules/ 100 | }, 101 | { 102 | test: /\.jsx?$/, 103 | use: { 104 | loader: 'babel-loader', 105 | query: { //同时可以把babel配置写到根目录下的.babelrc中 106 | presets: ['env', 'stage-0'] // env转换es6 stage-0转es7 107 | } 108 | } 109 | }, 110 | { //file-loader 解决css等文件中引入图片路径的问题 111 | // url-loader 当图片较小的时候会把图片BASE64编码,大于limit参数的时候还是使用file-loader 进行拷贝 112 | test: /\.(png|jpg|jpeg|gif|svg)/, 113 | use: { 114 | loader: 'url-loader', 115 | options: { 116 | outputPath: 'images/', // 图片输出的路径 117 | limit: 1 * 1024 118 | } 119 | } 120 | } 121 | ] 122 | }, 123 | plugins: [ 124 | // 多入口的html文件用chunks这个参数来区分 125 | new HtmlWebpackPlugin({ 126 | template: path.resolve(__dirname,'src','index.html'), 127 | filename:'index.html', 128 | chunks:['index', 'vendor'], 129 | hash:true,//防止缓存 130 | minify:{ 131 | removeAttributeQuotes:true//压缩 去掉引号 132 | } 133 | }), 134 | new HtmlWebpackPlugin({ 135 | template: path.resolve(__dirname,'src','page.html'), 136 | filename:'page.html', 137 | chunks:['page', 'vendor'], 138 | hash:true,//防止缓存 139 | minify:{ 140 | removeAttributeQuotes:true//压缩 去掉引号 141 | } 142 | }), 143 | new webpack.ProvidePlugin({ 144 | _:'lodash' //所有页面都会引入 _ 这个变量,不用再import引入 145 | }), 146 | new ExtractTextWebapckPlugin('css/[name].[hash].css'), // 其实这个特性只用于打包生产环境,测试环境这样设置会影响HMR 147 | new CopyWebpackPlugin([ 148 | { 149 | from: path.resolve(__dirname, 'static'), 150 | to: path.resolve(__dirname, 'dist/static'), 151 | ignore: ['.*'] 152 | } 153 | ]), 154 | new CleanWebpackPlugin([path.join(__dirname, 'dist')]), 155 | ], 156 | devtool: 'eval-source-map', // 指定加source-map的方式 157 | devServer: { 158 | contentBase: path.join(__dirname, "dist"), //静态文件根目录 159 | port: 3824, // 端口 160 | host: 'localhost', 161 | overlay: true, 162 | compress: false // 服务器返回浏览器的时候是否启动gzip压缩 163 | }, 164 | watch: true, // 开启监听文件更改,自动刷新 165 | watchOptions: { 166 | ignored: /node_modules/, //忽略不用监听变更的目录 167 | aggregateTimeout: 500, //防止重复保存频繁重新编译,500毫米内重复保存不打包 168 | poll:1000 //每秒询问的文件变更的次数 169 | }, 170 | } 171 | ``` 172 | 173 | 在命令行下用以下命令安装`loader`和依赖的插件,生成完全的`package.json`项目依赖树。 174 | 175 | ```bash 176 | npm install extract-text-webpack-plugin@next --save-dev 177 | npm i style-loader css-loader postcss-loader --save-dev 178 | npm i less less-loader --save-dev 179 | npm i node-sass sass-loader --save-dev 180 | npm i babel-core babel-loader babel-preset-env babel-preset-stage-0 --save-dev 181 | npm i file-loader url-loader --save-dev 182 | 183 | npm i html-webpack-plugin ---save-dev 184 | npm i clean-webpack-plugin --save-dev 185 | npm i copy-webpack-plugin --save-dev 186 | 187 | npm run dev 188 | ``` 189 | 190 | 默认打开的页面是`index.html`页面,可以加上/page.html来打开page页面看效果。 191 | PS: 关于`loader`的详细说明可以参考`webpack3.x`的学习介绍,上面配置中需要注意的是多页面的公共库的引入采用的是`vendor`+暴露全局变量的方式,其实这种方式有诸多弊端,而`webpack4`针对这种情况设置了新的API,有兴趣的话,就继续看下面的高级配置吧。 192 | 193 | ### 进阶的webpack4配置搭建 194 | 195 | 包含以下几个方面: 196 | 1. 针对`CSS`和`JS`的`TreeShaking`来减少无用代码,针对`JS`需要对已有的`uglifyjs`进行一些自定义的配置(生产环境配置) 197 | 2. 新的公共代码抽取工具(`optimization.SplitChunksPlugin`)提取重用代码,减小打包文件。(代替`commonchunkplugin`,生产和开发环境都需要) 198 | 3. 使用`HappyPack`进行`javascript`的多进程打包操作,提升打包速度,并增加打包时间显示。(生产和开发环境都需要) 199 | 4. 创建一个`webpack.dll.config.js`文件打包常用类库到dll中,使得开发过程中基础模块不会重复打包,而是去动态连接库里获取,代替上一节使用的`vendor`。(注意这个是在开发环境使用,生产环境打包对时间要求并不高,后者往往是项目持续集成的一部分) 200 | 5. 模块热替换,还需要在项目中增加一些配置,不过大型框架把这块都封装好了。(开发环境配置) 201 | 6. `webpack3`新增的作用域提升会默认在`production`模式下启用,不用特别配置,但只有在使用ES6模块才能生效。 202 | 203 | 关于第四点,需要在package.json中的script中增加脚本: 204 | `"build:dll": "webpack --config webpack.dll.config.js --mode development",` 205 | 206 | 补充安装插件的命令行: 207 | 208 | ```bash 209 | npm i purify-css purifycss-webpack -D // 用于css的tree-shaking 210 | npm i webpack-parallel-uglify-plugin -D // 用于js的tree-shaking 211 | npm i happypack@next -D //用于多进程打包js 212 | npm i progress-bar-webpack-plugin -D //用于显示打包时间和进程 213 | npm i webpack-merge -D //优化配置代码的工具 214 | npm i optimize-css-assets-webpack-plugin -D //压缩CSS 215 | npm i chalk -D 216 | npm install css-hot-loader -D // css热更新 217 | npm i mini-css-extract-plugin -D 218 | npm i cross-env -D 219 | ``` 220 | 221 | `TreeShaking`需要增加的配置代码,这一块参考[`webpack`文档](https://webpack.js.org/guides/tree-shaking/),需要三方面因素,分别是: 222 | 223 | - 使用`ES6`模块(`import/export`) 224 | - 在`package.json`文件中声明`sideEffects`指定可以`treeShaking`的模块 225 | - 启用`UglifyJSPlugin`,多入口下用`WebpackParallelUglifyPlugin`(这是下面的配置代码做的事情) 226 | 227 | ```javascript 228 | /*最上面要增加的声明变量*/ 229 | const glob = require('glob') 230 | const PurifyCSSPlugin = require('purifycss-webpack') 231 | const WebpackParallelUglifyPlugin = require('webpack-parallel-uglify-plugin') 232 | 233 | /*在`plugins`配置项中需要增加的两个插件设置*/ 234 | new PurifyCSSPlugin({ 235 | paths: glob.sync(path.join(__dirname, 'src/*.html')) 236 | }), 237 | new WebpackParallelUglifyPlugin({ 238 | uglifyJS: { 239 | output: { 240 | beautify: false, //不需要格式化 241 | comments: false //不保留注释 242 | }, 243 | compress: { 244 | warnings: false, // 在UglifyJs删除没有用到的代码时不输出警告 245 | drop_console: true, // 删除所有的 `console` 语句,可以兼容ie浏览器 246 | collapse_vars: true, // 内嵌定义了但是只用到一次的变量 247 | reduce_vars: true // 提取出出现多次但是没有定义成变量去引用的静态值 248 | } 249 | } 250 | // 有兴趣可以探究一下使用uglifyES 251 | }), 252 | ``` 253 | 254 | 关于`ES6`模块这个事情,上文的第六点也提到了只有`ES6`模块写法才能用上最新的作用域提升的特性,首先`webpack4.x`并不需要额外修改`babelrc`的配置来实现去除无用代码,这是从`webpack2.x`升级后支持的,改用`sideEffect`声明来实现。但作用域提升仍然需要把`babel`配置中的`module`转换去掉,修改后的`.babelrc`代码如下: 255 | 256 | ```json 257 | { 258 | "presets": [["env", {"loose": true, "modules": false}], "stage-0"] 259 | } 260 | ``` 261 | 262 | 但这个时候会发现`import`引入样式文件就被去掉了……只能使用`require`来改写了。 263 | 264 | 打包`DLL`第三方类库的配置项,用于开发环境: 265 | 266 | 1. `webpack.dll.config.js`配置文件具体内容: 267 | 268 | ```js 269 | const path = require('path') 270 | const webpack = require('webpack') 271 | const pkg = require('../package.json') 272 | /** 273 | * 尽量减小搜索范围 274 | * target: '_dll_[name]' 指定导出变量名字 275 | */ 276 | module.exports = { 277 | context: path.resolve(__dirname, '../'), 278 | entry: { 279 | vendor: Object.keys(pkg.dependencies) 280 | }, 281 | output: { 282 | path: path.join(__dirname, 'dist'), 283 | filename: '[name].dll.js', 284 | library: '_dll_[name]' // 全局变量名,其他模块会从此变量上获取里面模块 285 | }, 286 | // manifest是描述文件 287 | plugins: [ 288 | new webpack.DllPlugin({ 289 | name: '_dll_[name]', 290 | path: path.join(__dirname, 'dist', 'manifest.json'), 291 | context: path.resolve(__dirname, '../') 292 | }) 293 | ] 294 | } 295 | ``` 296 | 297 | 2. 在`webpack.config.js`中增加的配置项: 298 | 299 | ```javascript 300 | /*找到上一步生成的`manifest.json`文件配置到`plugins`里面*/ 301 | new webpack.DllReferencePlugin({ 302 | manifest: require(path.join(__dirname, '..', 'dist', 'manifest.json')), 303 | }), 304 | ``` 305 | 306 | 多文件入口的公用代码提取插件配置: 307 | 308 | ```javascript 309 | /*webpack4.x的最新优化配置项,用于提取公共代码,跟`entry`是同一层级*/ 310 | optimization: { 311 | splitChunks: { 312 | cacheGroups: { 313 | commons: { 314 | chunks: "initial", 315 | name: "common", 316 | minChunks: 2, 317 | maxInitialRequests: 5, 318 | minSize: 0 319 | } 320 | } 321 | } 322 | } 323 | 324 | /*针对生成HTML的插件,需增加common,也去掉上一节加的vendor*/ 325 | new HtmlWebpackPlugin({ 326 | template: path.resolve(__dirname,'src','index.html'), 327 | filename:'index.html', 328 | chunks:['index', 'common'], 329 | vendor: './vendor.dll.js', //与dll配置文件中output.fileName对齐 330 | hash:true,//防止缓存 331 | minify:{ 332 | removeAttributeQuotes:true//压缩 去掉引号 333 | } 334 | }), 335 | new HtmlWebpackPlugin({ 336 | template: path.resolve(__dirname,'src','page.html'), 337 | filename:'page.html', 338 | chunks:['page', 'common'], 339 | vendor: './vendor.dll.js', //与dll配置文件中output.fileName对齐 340 | hash:true,//防止缓存 341 | minify:{ 342 | removeAttributeQuotes:true//压缩 去掉引号 343 | } 344 | }), 345 | ``` 346 | 347 | PS: 这一块要多注意,对应入口的`HTML`文件也要处理,关键是自定义的`vendor`项,在开发环境中引入到`html`中 348 | 349 | `HappyPack`的多进程打包处理: 350 | 351 | ```javascript 352 | /*最上面要增加的声明变量*/ 353 | const HappyPack = require('happypack') 354 | const os = require('os') //获取电脑的处理器有几个核心,作为配置传入 355 | const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length }) 356 | const ProgressBarPlugin = require('progress-bar-webpack-plugin') 357 | 358 | /*在`module.rules`配置项中需要更改的`loader`设置*/ 359 | { 360 | test: /\.jsx?$/, 361 | loader: 'happypack/loader?id=happy-babel-js', 362 | include: [path.resolve('src')], 363 | exclude: /node_modules/, 364 | }, 365 | 366 | /*在`plugins`配置项中需要增加的插件设置*/ 367 | new HappyPack({ //开启多线程打包 368 | id: 'happy-babel-js', 369 | loaders: ['babel-loader?cacheDirectory=true'], 370 | threadPool: happyThreadPool 371 | }), 372 | new ProgressBarPlugin({ 373 | format: ' build [:bar] ' + chalk.green.bold(':percent') + ' (:elapsed seconds)' 374 | }) 375 | ``` 376 | 377 | PS:要记住这种使用方法下一定要在根目录下加`.babelrc`文件来设置`babel`的打包配置。 378 | 379 | 开发环境的代码热更新: 380 | 其实针对热刷新,还有两个方面要提及,一个是html文件里面写代码的热跟新(这个对于框架不需要,如果要实现,建议使用`glup`,后面有代码),一个是写的样式代码的热更新,这两部分也要加进去。让我们一起看看热更新需要增加的配置代码: 381 | 382 | ```javascript 383 | /*在`devServer`配置项中需增加的设置*/ 384 | hot:true 385 | 386 | /*在`plugins`配置项中需要增加的插件设置*/ 387 | new webpack.HotModuleReplacementPlugin(), //模块热更新 388 | new webpack.NamedModulesPlugin(), //模块热更新 389 | ``` 390 | 391 | 在业务代码中要做一些改动,一个比较`low`的例子为: 392 | 393 | ```javascript 394 | if(module.hot) { //设置消息监听,重新执行函数 395 | module.hot.accept('./hello.js', function() { 396 | div.innerHTML = hello() 397 | }) 398 | } 399 | ``` 400 | 401 | 但还是不能实现在`html`修改后自动刷新页面,这里有个概念是热更新不是针对页面级别的修改,这个问题有一些解决方法,但目前都不是很完美,可以参考[这里](https://stackoverflow.com/questions/33183931/how-to-watch-index-html-using-webpack-dev-server-and-html-webpack-plugin),现在针对CSS的热重载有一套解决方案如下,需要放弃使用上文提到的`ExtractTextWebapckPlugin`,引入`mini-css-extract-plugin`和`hot-css-loader`来实现,前者在webpack4.x上与`hot-css-loader`有报错,让我们改造一番: 402 | 403 | ```javascript 404 | /*最上面要增加的声明变量*/ 405 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 406 | 407 | /*在样式的`loader`配置项中需增加的设置,实现css热更新,以css为例,其他可以参照我的仓库来写*/ 408 | { 409 | test: /\.css$/, 410 | use: ['css-hot-loader', MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'], 411 | include: [resolve('src')], //限制范围,提高打包速度 412 | exclude: /node_modules/ 413 | } 414 | 415 | /*在`plugins`配置项中需要增加的插件设置,注意这里不能写[hash],否则无法实现热跟新,如果有hash需要,可以开发环境和生产环境分开配置*/ 416 | new MiniCssExtractPlugin({ 417 | filename: "[name].css", 418 | chunkFilename: "[id].css" 419 | }) 420 | ``` 421 | 422 | 用于生产环境压缩`css`的插件,看官方文档说明,样式文件压缩没有内置的,所以暂时引用第三方插件来做,以下是配置示例。 423 | 424 | ```js 425 | /*要增加的声明变量*/ 426 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 427 | 428 | /*在`plugins`配置项中需要增加的插件设置*/ 429 | new OptimizeCSSPlugin({ 430 | cssProcessorOptions: {safe: true} 431 | }) 432 | ``` 433 | 434 | ### 最终成果 435 | 436 |   在进阶部分我们对`webpack`配置文件根据开发环境和生产环境的不同做了分别的配置,因此有必要分成两个文件,然后发现重复的配置代码很多,作为有代码洁癖的人不能忍,果断引入`webpack-merge`,来把相同的配置抽出来,放到`build/webpack.base.js`中,而后在`build/webpack.dev.config.js`(开发环境)和`build/webpack.prod.config.js`(生产环境)中分别引用,在这个过程中也要更改之前文件的路径设置,以免打包或者找文件的路径出错,同时将`package.json`中的脚本命令修改为: 437 | 438 | ```json 439 | "scripts": { 440 | "build": "cross-env NODE_ENV='production' webpack --config build/webpack.prod.config.js --mode production", 441 | "dev": "cross-env NODE_ENV='development' webpack-dev-server --open --config build/webpack.dev.config.js --mode development", 442 | "dll": "webpack --config build/webpack.dll.config.js --mode production", 443 | "start": "npm run dll && npm run dev", 444 | "prod": "npm run dll && npm run build" 445 | } 446 | ``` 447 | 448 | 接下来就是代码的重构过程,这个过程其实我建议大家自己动手做一做,就能对`webpack`配置文件结构更加清晰。 449 | 450 | `build`文件夹下的`webpack.base.js`文件: 451 | 452 | ```js 453 | 'use strict' 454 | const path = require('path'); 455 | const chalk = require('chalk'); 456 | const ProgressBarPlugin = require('progress-bar-webpack-plugin') 457 | const HappyPack = require('happypack') 458 | const os = require('os') 459 | const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length }) 460 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 461 | function resolve (dir) { 462 | return path.join(__dirname, '..', dir) 463 | } 464 | 465 | function assetsPath(_path_) { 466 | let assetsSubDirectory; 467 | if (process.env.NODE_ENV === 'production') { // 这里需要用cross-env来注入Node变量 468 | assetsSubDirectory = 'static' //可根据实际情况修改 469 | } else { 470 | assetsSubDirectory = 'static' 471 | } 472 | return path.posix.join(assetsSubDirectory, _path_) 473 | } 474 | 475 | module.exports = { 476 | context: path.resolve(__dirname, '../'), 477 | entry: { 478 | index: './src/index.js', 479 | page: './src/page.js' 480 | }, 481 | output:{ 482 | path: resolve('dist'), 483 | filename:'[name].[hash].js' 484 | }, 485 | resolve: { 486 | extensions: [".js",".css",".json"], 487 | alias: {} //配置别名可以加快webpack查找模块的速度 488 | }, 489 | module: { 490 | // 多个loader是有顺序要求的,从右往左写,因为转换的时候是从右往左转换的 491 | rules:[ 492 | { 493 | test: /\.css$/, 494 | use: ['css-hot-loader', MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'], 495 | include: [resolve('src')], //限制范围,提高打包速度 496 | exclude: /node_modules/ 497 | }, 498 | { 499 | test:/\.less$/, 500 | use: ['css-hot-loader', MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader'], 501 | include: [resolve('src')], 502 | exclude: /node_modules/ 503 | }, 504 | { 505 | test:/\.scss$/, 506 | use: ['css-hot-loader', MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'sass-loader'], 507 | include: [resolve('src')], 508 | exclude: /node_modules/ 509 | }, 510 | { 511 | test: /\.jsx?$/, 512 | loader: 'happypack/loader?id=happy-babel-js', 513 | include: [resolve('src')], 514 | exclude: /node_modules/, 515 | }, 516 | { //file-loader 解决css等文件中引入图片路径的问题 517 | // url-loader 当图片较小的时候会把图片BASE64编码,大于limit参数的时候还是使用file-loader 进行拷贝 518 | test: /\.(png|jpg|jpeg|gif|svg)/, 519 | use: { 520 | loader: 'url-loader', 521 | options: { 522 | name: assetsPath('images/[name].[hash:7].[ext]'), // 图片输出的路径 523 | limit: 1 * 1024 524 | } 525 | } 526 | }, 527 | { 528 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 529 | loader: 'url-loader', 530 | options: { 531 | limit: 10000, 532 | name: assetsPath('media/[name].[hash:7].[ext]') 533 | } 534 | }, 535 | { 536 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 537 | loader: 'url-loader', 538 | options: { 539 | limit: 10000, 540 | name: assetsPath('fonts/[name].[hash:7].[ext]') 541 | } 542 | } 543 | ] 544 | }, 545 | optimization: { //webpack4.x的最新优化配置项,用于提取公共代码 546 | splitChunks: { 547 | cacheGroups: { 548 | commons: { 549 | chunks: "initial", 550 | name: "common", 551 | minChunks: 2, 552 | maxInitialRequests: 5, // The default limit is too small to showcase the effect 553 | minSize: 0 // This is example is too small to create commons chunks 554 | } 555 | } 556 | } 557 | }, 558 | plugins: [ 559 | new HappyPack({ 560 | id: 'happy-babel-js', 561 | loaders: ['babel-loader?cacheDirectory=true'], 562 | threadPool: happyThreadPool 563 | }), 564 | new MiniCssExtractPlugin({ 565 | filename: "[name].css", 566 | chunkFilename: "[id].css" 567 | }), 568 | new ProgressBarPlugin({ 569 | format: ' build [:bar] ' + chalk.green.bold(':percent') + ' (:elapsed seconds)' 570 | }), 571 | ] 572 | } 573 | ``` 574 | 575 | `webpack.dev.config.js`文件内容: 576 | 577 | ```javascript 578 | const path = require('path'); 579 | const HtmlWebpackPlugin = require('html-webpack-plugin') // 生成html的插件 580 | const webpack = require('webpack') 581 | const baseConfig = require('./webpack.base') 582 | const merge = require('webpack-merge') 583 | 584 | const devWebpackConfig = merge(baseConfig, { 585 | output:{ 586 | publicPath: '/' 587 | }, 588 | devtool: 'eval-source-map', // 指定加source-map的方式 589 | devServer: { 590 | inline:true,//打包后加入一个websocket客户端 591 | hot:true,//热加载 592 | contentBase: path.join(__dirname, "..", "dist"), //静态文件根目录 593 | port: 3824, // 端口 594 | host: 'localhost', 595 | overlay: true, 596 | compress: false // 服务器返回浏览器的时候是否启动gzip压缩 597 | }, 598 | watchOptions: { 599 | ignored: /node_modules/, //忽略不用监听变更的目录 600 | aggregateTimeout: 500, //防止重复保存频繁重新编译,500毫米内重复保存不打包 601 | poll:1000 //每秒询问的文件变更的次数 602 | }, 603 | plugins: [ 604 | // 多入口的html文件用chunks这个参数来区分 605 | new HtmlWebpackPlugin({ 606 | template: path.resolve(__dirname, '..', 'src','index.html'), 607 | filename:'index.html', 608 | chunks:['index', 'common'], 609 | vendor: './vendor.dll.js', //与dll配置文件中output.fileName对齐 610 | hash:true,//防止缓存 611 | minify:{ 612 | removeAttributeQuotes:true//压缩 去掉引号 613 | } 614 | }), 615 | new HtmlWebpackPlugin({ 616 | template: path.resolve(__dirname, '..', 'src','page.html'), 617 | filename:'page.html', 618 | chunks:['page', 'common'], 619 | vendor: './vendor.dll.js', //与dll配置文件中output.fileName对齐 620 | hash:true,//防止缓存 621 | minify:{ 622 | removeAttributeQuotes:true//压缩 去掉引号 623 | } 624 | }), 625 | new webpack.DllReferencePlugin({ 626 | manifest: path.resolve(__dirname, '..', 'dist', 'manifest.json') 627 | }), 628 | new webpack.HotModuleReplacementPlugin(), //HMR 629 | new webpack.NamedModulesPlugin() // HMR 630 | ] 631 | }) 632 | 633 | module.exports = devWebpackConfig 634 | ``` 635 | 636 | `webpack.dev.config.js`文件内容: 637 | 638 | ```javascript 639 | 'use strict' 640 | const path = require('path'); 641 | const CopyWebpackPlugin = require('copy-webpack-plugin') // 复制静态资源的插件 642 | const CleanWebpackPlugin = require('clean-webpack-plugin') // 清空打包目录的插件 643 | const HtmlWebpackPlugin = require('html-webpack-plugin') // 生成html的插件 644 | const webpack = require('webpack') 645 | const baseConfig = require('./webpack.base') 646 | const merge = require('webpack-merge') 647 | 648 | const glob = require('glob') 649 | const PurifyCSSPlugin = require('purifycss-webpack') 650 | const WebpackParallelUglifyPlugin = require('webpack-parallel-uglify-plugin') 651 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 652 | 653 | module.exports = merge(baseConfig, { 654 | output:{ 655 | publicPath: './' //这里要放的是静态资源CDN的地址(只在生产环境下配置) 656 | }, 657 | plugins: [ 658 | new HtmlWebpackPlugin({ 659 | template: path.resolve(__dirname, '..', 'src', 'index.html'), 660 | filename:'index.html', 661 | chunks:['index', 'common'], 662 | hash:true,//防止缓存 663 | minify:{ 664 | removeAttributeQuotes:true//压缩 去掉引号 665 | } 666 | }), 667 | new HtmlWebpackPlugin({ 668 | template: path.resolve(__dirname, '..', 'src', 'page.html'), 669 | filename:'page.html', 670 | chunks:['page', 'common'], 671 | hash:true,//防止缓存 672 | minify:{ 673 | removeAttributeQuotes:true//压缩 去掉引号 674 | } 675 | }), 676 | new CopyWebpackPlugin([ 677 | { 678 | from: path.join(__dirname, '..', 'static'), 679 | to: path.join(__dirname, '..', 'dist', 'static'), 680 | ignore: ['.*'] 681 | } 682 | ]), 683 | new CleanWebpackPlugin(['dist'], { 684 | root: path.join(__dirname, '..'), 685 | verbose: true, 686 | dry: false 687 | }), 688 | new OptimizeCSSPlugin({ 689 | cssProcessorOptions: {safe: true} 690 | }), 691 | new PurifyCSSPlugin({ 692 | paths: glob.sync(path.join(__dirname, '../src/*.html')) 693 | }), 694 | new WebpackParallelUglifyPlugin({ 695 | uglifyJS: { 696 | output: { 697 | beautify: false, //不需要格式化 698 | comments: false //不保留注释 699 | }, 700 | compress: { 701 | warnings: false, // 在UglifyJs删除没有用到的代码时不输出警告 702 | drop_console: true, // 删除所有的 `console` 语句,可以兼容ie浏览器 703 | collapse_vars: true, // 内嵌定义了但是只用到一次的变量 704 | reduce_vars: true // 提取出出现多次但是没有定义成变量去引用的静态值 705 | } 706 | } 707 | }), 708 | ] 709 | }) 710 | ``` 711 | 712 | 多说一句,就是实现JS打包的`treeShaking`还有一种方法是编译期分析依赖,利用uglifyjs来完成,这种情况需要保留ES6模块才能实现,因此在使用这一特性的仓库中,`.babelrc`文件的配置为:`"presets": [["env", { "modules": false }], "stage-0"]`,就是打包的时候不要转换模块引入方式的含义。 713 | 714 | 接下来就可以运行`npm start`,看一下进阶配置后的成果啦,吼吼,之后只要不进行`build`打包操作,通过`npm run dev`启动,不用重复打包`vendor`啦。生产环境打包使用的是`npm run build`。 715 | 716 | 以上就是对`webpack4.x`配置的踩坑过程,期间参考了大量谷歌英文资料,希望能帮助大家更好地掌握`wepback`最新版本的配置,以上内容亲测跑通,有问题的话,欢迎加我微信(kashao3824)讨论,到[`github`地址](https://github.com/wlx200510/webpack4.x-learn)提`issue`也可,欢迎`fork/star`。 717 | 718 | 最新更改: 719 | 720 | - 修复了`common`会重复打包已有`dll`库的问题 721 | - 现在的`dll`库会自动根据`package.json`中的配置项生成 722 | - `dll`现在是生产环境打包模式,并且`vendor.dll.js`现在在生产环境下也会注入`HTML`模板中 723 | - 生产环境打包使用命令`npm run prod` 724 | - 修复了`process.env.NODE_ENV`在打包过程中取不到的问题 [issue2](https://github.com/wlx200510/webpack4.x-learn/issues/2) --------------------------------------------------------------------------------