大前端之路 -webpack 速成(一)

503 阅读8分钟

写作本文的目的

本人是一名android开发,最近在往大前端转,讲白了之前是做移动开发的,web前端技能缺失。为了扩充技术栈vue+css布局。 当然仅仅有这2个技能是不够的,要迅速参与前端组的业务开发甚至独立承担一个小模块的开发,我们当然不能 不会webpack。就好比android开发 如果不会配置gradle 那肯定也是不合格的。

适合阅读本文的群体

适合前端初学者,尤其是有移动端开发经验的。

阅读本文能得到什么?

迅速掌握 webpack的主流配置,让你参与到实际业务开发的时候不会一脸懵逼的问这是什么,那是什么? 至少能明白 webpack的一些基本配置 是为了解决哪些问题而引入的。

怎么看 webpack config 文件?

webpack.config.js

// 引入一个node的核心模块 因为下面的 path 参数 后面只能跟绝对路径
// 所以必须利用这个path的模块 来让我们可以写入一个相对路径
const path = require('path')

module.exports = {
    // 打包的入口文件 这里可以直接写相对路径
    entry: './index.js',
    output: {
        // 定义打包以后输出文件的 文件名 如果不设置的话 默认就是main.js
        filename: 'bundle.js',
        //这里就是定义打包以后输出的文件夹的名字, 这里设置成bundle 前面这个函数调用就是为了 生成绝对路径用的
        //因为这里的path 参数 只接受绝对路径
        path: path.resolve(__dirname, 'bundle')
    }
}

如果不想用这个默认的文件,我们当然可以指定一个打包的设置文件:

npx webpack --config webpack2.config.js

如此,就可以用这个非默认的config 文件 进行打包了。

npx webpack 这个命令为啥用的人不多?

答案就在这

通常,我们会在这边设置一下 我们的打包快捷命令。

设置之前 我是这样打包的:

设置一下:

 "scripts": {
    "bundle": "webpack"
  },

然后输入命令:

npm run bundle

即可:

安装webpack的时候 为什么要安装webpack-cli

简单来说 webpack-cli 就是 webpack的命令行工具,你不安装这个cli 你打那些webpack的命令 也就没什么卵用了

webpack 打包出来的结果怎么看?

bundle.js 就是我们打包出来的js文件,很好理解。 chunks 是这个js文件的id,如果工程很大的话,这里会有很多结果文件,每个文件都会有个id。

注意看这个chunkNames 他为啥值是main? 实际上还是跟我们的webpack config配置文件有关

继续看:

第一个地方就是打包出来的结果js文件,是由哪些源文件组成的。

第二个地方是一堆黄色的警告信息:

其实就是警告你没有手动设置打包mode的(生产环境(压缩js代码到一行)和开发环境),

webpack 如何打包静态图片?

默认的webpack 是只能打包js文件的,并不知道怎么打包图片文件,所以一般都要我们配置一下: 去webpack config 文件下 增加如下配置:

  module: {
        rules: [{
            test: /\.jpg$/, 
            use: {
                loader: 'file-loader'//配置打包jpg文件的loader
            }
        }]
    }

然后别忘记 install 一下这个loader:

cnpm install file-loader -D

打包结果如下:

顺便提一下 要想学习好webpack 一定要会阅读官方文档,例如 这个file-loader:

总结一下 我们file-loader的 行为特征:

  1. 发现我们引入了图片以后, 将图片 拷贝到dist目录下
  2. 拷贝结束以后 会把文件重命名(这个重命名的规则其实也可以自定义)
  3. 新的文件的地址 再返回给我们代码里面的变量 即可。
  4. file-loader 可以处理一切静态文件。

再次强调一下,webpack默认只能打包js文件,如果有非js文件的,就必须引入对应的loader,比如静态图片文件的 file-loader, vue文件 对应的 vueloader 等等。

loader 打包图片静态资源

其实也是可以设置成 打包以后的文件名 命名规则的,比如这样就可以设置打包以后的图片名称 不要变:

可以去官网看这些placeholder 都有哪些

稍微修改一下配置参数 ,让他支持更多有用的特性:


    module: {
        rules: [{
            //可以支持很多文件格式 改一下这个正则表达式即可
            test: /\.(jpg|png|gif)$/,
            use: {
                loader: 'file-loader',
                options: {
                    //打包的文件名可以支持多种 placeholder 可以所以组合
                    name: '[name]_[hash].[ext]',
                    //这里就是设置我们的打包目的路径 
                    outputPath:'images/'
                },
            }
        }]
    }

如此以后,打包结果就变成:

现在使用更多的其实是url-loader ,他可以实现file-loader的一切功能。

cnpm install url-loader -D
 module: {
        rules: [{
            test: /\.(jpg|png|gif)$/,
            use: {
                //这里默认情况下 url-loader 会把图片转成base4编码 可以省略一次http请求,
                // 但是缺点是 如果图片很大的话,这个base64编码也很大,js文件就太大了,
                loader: 'url-loader',
                options: {
                    name: '[name]_[hash].[ext]',
                    outputPath:'images/'
                },
            }
        }]
    }

所以 这里 我们要做个改动,判断一下 如果图片很小 就转成base64 编码,如果图片超过这个大小 那么就还是打包成静态文件 而不是转码

 module: {
        rules: [{
            test: /\.(jpg|png|gif)$/,
            use: {
                //这里默认情况下 url-loader 会把图片转成base4编码 可以省略一次http请求,
                // 但是缺点是 如果图片很大的话,这个base64编码也很大,js文件就太大了,
                loader: 'url-loader',
                options: {
                    name: '[name]_[hash].[ext]',
                    outputPath: 'images/',
                    //加入这个阈值,小于他 就base64转码 省略一次http请求
                    //大于他 就是一个静态图片打包
                    limit: 2048
                },
            }
        }]
    }

你可以认为 url-loader 就是比 file-loader 多了一个limit的配置项,可以支持打包成base64编码的图片。

loader 打包css 资源

 module: {
        rules: [{
            test: /\.(jpg|png|gif)$/,
            use: {
                loader: 'url-loader',
                options: {
                    name: '[name]_[hash].[ext]',
                    outputPath: 'images/',
                   
                    limit: 2048
                },
            }
        },{
            test: /\.css$/,
            use: ['style-loader','css-loader']//css 的loader 一般都要配置两个 所以这里是数组
        }]
    }

记得不要忘记安装这2个css loader

cnpm install style-loader css-loader -D

下面来简述一下 这2个 loader的 作用,我们先来看看css-loader的作用,

定义我们的avatar.css文件

.avatar{
    width: 150px;
    height: 150px;
}

然后定义一个index.css 文件

@import './avatar.css'

仅此而已,css-loader的作用就是 将import过来的css 文件 放到index.css文件中。

真正使用的地方在js 中

import avatar from './tly.jpg';
import './index.css';

var img = new Image()
img.src = avatar
img.classList.add('avatar')

var root = document.getElementById("div")
root.append(img)

然后最终打包出来以后我们看下html的源码:

所以style-loader的作用 最终就是将我们的css内容 都放到header的 style中,也就是将css挂载到html上的欧洲用

当然了,如果你项目里还用了诸如sass这种 css的写法 你还需要引入额外的sass-loader ,postcss-loader 可以自动加上css3 前置这里因为篇幅的原因 就不再展开了,大家知道有这么个东西就可以。

这里要注意的是,loader的执行顺序 是根据你代码里配置的顺序来的,所以顺序千万不能乱。 顺序是 从下往上,从右往左。 也就是说是** use 数组里的逆序**

module: {
        rules: [{
            test: /\.(jpg|png|gif)$/,
            use: {
                loader: 'url-loader',
                options: {
                    name: '[name]_[hash].[ext]',
                    outputPath: 'images/',

                    limit: 2048
                },
            }
        }, {
            test: /\.css$/,
            use: ['style-loader', {
                loader: 'css-loader',
                options: {
                    // 这里的配置的意思是 假设一个sass文件里面  引入的js文件 里面也用到了import 语句,
                    // 那么此时就让loader 再次回溯到上层postcss-loader 开始解析,这样能保证嵌套引用的正确性
                    //这种写法 在很多项目中都有。
                    importLoaders: 2,
                     modules:true //开启模块化css
                }
            },'sass-loader','postcss-loader']
        }]
    }

注意这个modules:true 他其实就是模块化打包css 用的。

import style from './index.scss'
img.classList.add(style.avatar)

如果是这样引入 这个avatar的css样式,那么就不会污染整个界面。 讲白了 和java里面 类名一样,但是包名不同的概念是一样的。

另外如果要打包font ,注意使用file-loader 即可。管理静态资源可以从这里找

HtmlWebpackPlugin 是干嘛的?

看下之前的项目结构,我们之前的项目的html文件 是直接放在dist 这个目录下的。包括index.html 所引用的js文件的文件名 也是dist 下的 打包出来的 js的文件 文件名。

这会带来一个问题,dist打包目录下的html文件 是我们开发的html。而dist目录下 其他的文件却是打包出来的生产环境下的产物。很不协调 也很不方便。

所以就诞生了htmlwebpackplugin 这个插件 。他可以让我们在src目录下 开发你的html,引用的js文件 就是你src下的js文件名,然后打包以后 自动的 在dist 也生成你的index.html 并自动将你引用的js名称更换成打包以后的名称。

如图:

const path = require('path');
//引入插件 方便我们打包
const HtmlWebpackPlugin = require('html-webpack-plugin');
//每次打包 都删除我们的打包目录 你就把他理解成java 世界中的 clean project 即可
//这是个非官方的plugin 在webpack的官网上是找不到的
const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {
	mode: 'development',
	entry: {
		main: './src/index.js'
	},
	module: {
		rules: [{
			test: /\.(jpg|png|gif)$/,
			use: {
				loader: 'url-loader',
				options: {
					name: '[name]_[hash].[ext]',
					outputPath: 'images/',
					limit: 10240
				}
			} 
		}, {
			test: /\.(eot|ttf|svg)$/,
			use: {
				loader: 'file-loader'
			} 
		}, {
			test: /\.scss$/,
			use: [
				'style-loader', 
				{
					loader: 'css-loader',
					options: {
						importLoaders: 2
					}
				},
				'sass-loader',
				'postcss-loader'
			]
		}]
	},
	//引入插件
	plugins: [new HtmlWebpackPlugin({
		template: 'src/index.html'
	}), new CleanWebpackPlugin(['dist'])],
	output: {
		filename: 'bundle.js',
		path: path.resolve(__dirname, 'dist')
	}
}

plugin 有点类似于生命周期,也就是在你打包的某一些时刻 帮你做一些东西

entry和output 的基础设置

entry: {
		main: './src/index.js'
	},
output: {
		filename: 'bundle.js',
		path: path.resolve(__dirname, 'dist')
	}

这里要注意的是,这里是一个 一一对应的关系,你如果entry 里面还有其他的js文件,那么output哪里也必须配置成多个,这里是不允许多对1的关系的。

改写为:

entry: {
		main: './src/index.js',
		subitem:'./src/index.js'
	},
	
	output: {
		filename: '[name].js',
		path: path.resolve(__dirname, 'dist')
	}

这里的[name] 就是 entry 里面 key -value 键值对的 key值。我们可以看看打包以后的样式:

有时候,我们希望打包出来的 js 文件 是一个cdn的的地址。而不是我们服务器上相对路径的地址。

	output: {
		publicPath:'http://www.alicdn.om.cn',
		filename: '[name].js',
		path: path.resolve(__dirname, 'dist')
	}

这时候 我们重新打包 再看:

文件名还是那个文件名:

但是看看html中引用的js地址 就已经完全变化了:

项目中经常使用的也就是这么一些参数,还有其他的参数点我查看更多output参数说明

这个文档比较适合新手查看

管理好你的sourcemap

module.exports = {

	//开发者模式默认会打开src 为开发者目录
	mode: 'development',
	devtool:'source-map',
	entry: {
		main: './src/index.js'
	},

如果不配置这个东西 那么实际代码运行起来 出错的地方就很难找了,因为打包出来的js文件和你源码文件 压根不是一个东西,你打开chrome 控制台报错的js代码的位置 是指你打包出来的main.js 里面出错的位置, 并不是你源码中的位置。

所以一定要配置这个source-map,这样在开发过程中,万一有js代码报错 也一样能迅速定位到实际代码报错的位置。

官方文档 可以看看其他参数配置

这样打包出来的项目下 会多出一个map文件。 其实就是一个源文件和实际代码之间的映射关系。

和android里面每次打包出来以后的 mapping 文件 是一个意思。

一般而言,我们在开发期间development 更倾向于使用** cheap-module-eval-source-map** 这个参数。这种方式 提示的错误信息比较全,且打包速度也比较快。

如果是mode production 线上环境的代码,更倾向于使用 cheap-module-source-map 这个参数 这样提示效果更加好一点。

webpackdevserver 提升开发效率

看下现在的目录结构,我们现在在开发过程中,每次改完src中的代码 都得去命令行打一下 npm run bundle 然后 去disr目录下 把index.xml 在浏览器中打开。 这个流程是比较繁琐的。

第一种做法:

可以修改 我们的package.json 文件

"scripts": {
    "watch": "webpack --watch"
  },

这样 我们运行 npm run watch 命令:

现在的效果就是,只要你在src中改变了你的源代码,那么webpack 就会自动重新打包,是不需要你每次都去敲命令的。 但是在浏览器中 你还需要每次手动刷新页面才能看到最新的修改。

第二种做法: webpackdevserver

首先在 webpack config 文件下 增加配置

devServer: {
		contentBase: './dist'
	},

然后安装 webpack dev server,命令如下:

cnpm install webpack-dev-server -D

最后 修改一下我们的package.json 文件

"scripts": {
    "watch": "webpack --watch",
    "start": "webpack-dev-server"
  },

然后运行 npm run start 命令即可

如此以来,我们每次对源代码的修改 既不需要 每次命令行编译,也不需要 每次改完去刷新web页面了 现在就变成了 每次改完以后 页面会主动刷新,我们每次改完 都能及时的看到页面的变化。

当然还可以再次改进我们的 devserver 配置

devServer: {
		contentBase: './dist',
		//增加这个属性以后 每次我们npm run start 浏览器就会自动打开页面了 再也不需要第一次手动打开页面了
		open: true
	},

vue和react 脚手架 其实都有这个web dev server。他们的proxy其实就是这个

devserver 打包出来的东西 并没有写入到磁盘 dist目录中,而是直接加载在内存里

hmr 是什么?

import './style.css'
var btn = document.createElement('button')
btn.innerHTML = 'add'
document.body.appendChild(btn)
btn.onclick = function () {
    var div = document.createElement('div')
    div.innerHTML = 'item'
    document.body.appendChild(div)
}

div:nth-of-type(odd){
    background-color: blue;
}

先看上面的代码,其实很简单,就是每次add 都会添加一个div元素,然后 奇数的时候回有背景色。

假设我们此时在webdevserver 模式下开发, 已经add了不少个元素,这个时候 我们修改了css代码里的颜色 此时我们的界面就会被完全刷新,之前页面的状态就完全没有了。。。 又回到了初始化的状态

为了解决这个开发中的痛点,我们需要再webpack config 文件中 做一些修改:

//增加hmr 组件
const webpack = require('webpack')

plugins: [new HtmlWebpackPlugin({
		template: 'src/index.html'
	}), new CleanWebpackPlugin(['dist']),new webpack.HotModuleReplacementPlugin()],

devServer: {

		contentBase: './dist',
		open: true,
		hot:true, //开启hmr 模式
		hotOnly:true //就算hmr 没有生效 也不刷新浏览器
	},

如此,即可。

可以看出来 hmr 可以很方便的让我们调试css样式。 在这一点上我感觉是比android开发的体验更好的,android里面你要想改一个ui,必须重新编译 重新run,然后还得一步步操作到那个ui 界面才能看到最终的效果。。。。

同样的对调试某些js代码,这个hmr也可以发挥作用。毕竟你只要刷新了界面 你js里面那些变量都都会到初始化的值了。

考虑一种情况:

我们这里有2个cs文件 都被index js 文件引用。 其中counter的作用就是点击一次就改变一个文本的值。 而number的作用就是固定展示一个文本的值。

当我们每次修改number.js的时候 整个页面都会被刷新,这就会导致 之前我们counter.js 里面的执行的结果被刷新, 在有时候我们编写代码的时候 我不希望有类似的情况发生, 我只希望 我改动的那个js 代码块 刷新就可以了,其他没改的 保持不变。

当碰到这种情况的时候 我们只要:

import counter from './counter'
import number from './number'
counter()
number()

if(module.hot){
    //当开启hmr的时候,发现只要是number的模块 发生了变化 那么就重新执行一次 number的代码 即可
    module.hot.accept('./number',function(){
        number()
    })
}

其实css 也是这个原理,只不过这个部分的操作 是交给css-loader 来完成的,而对于js的代码,需要我们自己手动来写而已。 vue-loader 里面也有类似的代码。

关于hmr还有更多配置,详见

webpack 与 babel

es6转es5的 babel 使用起来规则 详见

这里要注意的是 babel-loader 只是将 babel的编译器与webpack打通起来,他本身并不是es6转es5代码的东西。

下面这行代码 才是

npm install @babel/preset-env --save-dev

额外配置

{ test: /\.js$/, exclude: /node_modules/, loader: "babel-loader", options: { presets: ["@babel/preset-env"] } }

仅仅这样还是不够的,还有一些es6的变量和函数没有转成es5

还需要引入 polyfill

也就是

cnpm install --save @babel/polyfill

然后 在我们使用es6 的js文件 开头 加上

//加入这条语句即可
import "@babel/polyfill";


const arr = [
    new Promise(() => { }),
    new Promise(() => { })
];

arr.map(item => {
    console.log(item);
})

这里我们看一下,开启不开启 polyfill 打包出来的main.js 的大小差距。

这其实主要是因为polyfill 默认是把全部的es6 特性 都帮你翻译成es5了,不管你用了其中的几个 他都会全量翻译。

我们如果想要的是 polyfill 只帮我们翻译我们使用到的那些东西,那就需要额外配置一个参数 useBuiltIns

{ test: /\.js$/, exclude: /node_modules/, loader: "babel-loader", options: { presets: [["@babel/preset-env",{
		    useBuiltIns:'usage'
		}]] } }

再看一下 打包出来的大小。

只有90多kb了。

我们甚至还可以给他配置 target 属性,可以指定 要支持的浏览器版本。因为浏览器不是一个版本把es6的特性全部支持完毕的,他也是分版本迭代的一个过程, 所以指定了target属性以后 他可以分辨 你要支持的最低版本,只翻译那些你用到并且浏览器还不支持的部分。 这样一来 打出来的js包 大小就会更小。

babel里面的内容 实际上 比 webpack的内容 更加晦涩难懂。其中大量涉及到ast 抽象语法树的知识。有兴趣的同学可以自行深入研究。

但是这里要注意的是,如果你在开发一个组件库,那么这种打包方式会污染到全局js。所以如果你是用babel 在开发一个组件库的话,得需要另外一种方式:transform打包

他可以有效避免 polyfill 全局污染的问题。写类库就要用这种方案了。