写作本文的目的
本人是一名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的 行为特征:
- 发现我们引入了图片以后, 将图片 拷贝到dist目录下
- 拷贝结束以后 会把文件重命名(这个重命名的规则其实也可以自定义)
- 新的文件的地址 再返回给我们代码里面的变量 即可。
- file-loader 可以处理一切静态文件。
再次强调一下,webpack默认只能打包js文件,如果有非js文件的,就必须引入对应的loader,比如静态图片文件的 file-loader, vue文件 对应的 vueloader 等等。
loader 打包图片静态资源
其实也是可以设置成 打包以后的文件名 命名规则的,比如这样就可以设置打包以后的图片名称 不要变:
稍微修改一下配置参数 ,让他支持更多有用的特性:
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 里面也有类似的代码。
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 全局污染的问题。写类库就要用这种方案了。