webpack(1)之基本配置

1,535 阅读8分钟

简介

  webpack是一个模块打包机,它会从指定入口文件开始,递归的寻找JavaScript模块以及其他一些浏览器无法直接运行的扩展语言(Sass, TypeScript)等,将其打包成合适的格式以供浏览器使用。
  它的作用有代码转换(利用各种loader将浏览器无法识别的语言转换成合适的格式),文件优化(比如说打包时压缩体积),代码分割,模块合并,自动刷新(热更新),代码校验,自动发布。 引用了网上的一张图来大致看一下webpack的运行机制:

webpack的一些基本配置

安装webpack以及默认配置

  首先是安装webpack,在本地项目文件夹下npm init初始化之后,下载webpack以及webpack-cli:

npm init -y
npm i webpack webpack-cli -D

  此时在文件夹下建立一个src文件夹,用于放置项目代码。webpack此时可以进行0配置打包,在命令行输入npx webpack可以打包出一个dist文件夹,下面有一个main.js就是打包后的文件。这个打包后的文件内容,就是使用递归的方式解析src中的js模块,递归的方法名为__webpack_require__,它支持我们在浏览器中使用CommonJs规范。
  默认打包的配置很弱,它只能识别js模块,在没有配置的情况下,webpack就相当于一个js模块打包机。当然我们不可能直接就0配置打包一个项目,下面我总结一下webpack中常用的一些基本配置。

webpack.config.js

  webpack中默认的配置文件名为webpack.config.js,在根目录下建立一个名为webpack.config.js的文件,就可以在这个文件中写配置项。它的内容遵循CommonJs规范,webpack提供给我们修改这个文件名的一些方法:
  (1)打包时输入命令npx webpack --config webpack.config.my.js
  (2)为了不用每次都在命令行输出一串这么长的命令,在package.json中配置scripts,"build" : "webpack --config webpack.config.my.js"
  这两种配置方法都可以修改默认配置文件名。先在webpack.config.my.js写一段基本的配置:

//webpack是用node写的
let path = require('path');
module.exports = {
    mode: 'development', //模式 生产环境production 开发模式development
    entry: './src/index.js', //入口
    output: { //出口
        filename: 'bundle.[hash:8].js', //打包后的文件名,[hash]每次打包生成新的文件
        //__dirname以当前目录解析成绝对路径
        path: path.resolve(__dirname, 'dist'), ///path字段只接受绝对路径,因此需要一个node模块来辅助配置 path.resolve把相对路径解析成绝对路径  
        publicPath: 'http://www.help.com'//公共路径,打包出的资源文件会带着这个公共路径。
    }
} 

Loader

  loader帮助我们告诉浏览器遇到不能识别的模块应该怎么处理,前面我们说过webpack默认配置只是别js模块,那么解析图片、css、less这些模块就需要引入loader。

打包图片(file-loader,url-loader)

  图片引入有三种方式:

  1. 在js中创建图片来引入,打包时要用到file-loader来解析图片地址加hash戳。
const logo = require('./01.jpg') //把图片引入,返回的结构是一个新的图片地址
const image = new Image();
console.log(logo); //用到file-loader 默认会在内部生成一张图片,到build目录下 把生成的图片的名字返回回来
image.src = logo;
document.body.appendChild(image)

  配置如下:

  • 使用file-loader:它解析静态资源,把原文件原封不动的拷贝一份放到打包后的文件夹下,并且把位置返回,上述logo的值就是该资源在打包后文件夹下的路径。一般可直接展示的文件用file-loader解析,比如图片,excel图表之类的。
{
    test: /\.(png|jpe?g|gif)$/,
    use: {
        loader: 'file-loader',
        options: {
            name:[name].[ext],
            outputPath: '/img/' //打包时另外生成一个img文件夹
        }
    }
},
  • 使用url-loader:它是file-loader的加强版,我们在它的配置项中可以设置一个limit值。如果文件体积小于该值,则文件被编码成Base64字符串直接引用,而不打包成一个新文件。这样可以减少一次http请求。如果文件大于该值,它的功能就相当于file-loader。这种方式会使打包后的文件体积变大,因此两者性能需要权衡。
{
    test: /\.(png|jpe?g|gif)$/,
    use: {
        loader: 'url-loader',
        options: {
            limit: 200 * 1024,//小于200k使用url-loader,大于200k使用file-loader
            outputPath: '/img/'
        }
    }
},
  1. 在css中添加图片,打包时css-loader会将url('./01.jpg')解析为url(require('./01.jpg'))。
body {
    background: red;
    background: url('./01.jpg')
}
  1. 在html中通过图片的img标签引入图片,打包时需要用到html-withimg-loader来解析图片地址。
<img src="./01.jpg" alt="">

  配置如下:

{
    test: /\.html$/,
    use: 'html-withimg-loader'
}
样式设置(css-loader,style-loader,less-loader)

  打包CSS文件,我们需要用到两个loader,一个是style-loader,它负责处理css文件中的import、url()语法。style-loader以内联<style>的形式将样式都写到模版html的<head>头部中。打包LESS文件同样的套路,less-loader现将less转换成css。
  配置如下:

module: {
    rules: [ 
        //loader的用法。字符串只用一个loader 多个loader需要用数组 loader的顺序 默认从右向左 从下往上执行
        {
            test: /\.css$/,
            use: [{
                loader: 'style-loader',
                options: {
                    insertAt: 'top'//插在最上面,让自己写在模版html<style>标签中的样式优先级较高
                }
            }, 'css-loader']
        },
        {
            test: /\.less$/,
            use: [{
                loader: 'style-loader',
                options: {
                    insertAt: 'top'
                }
            }, 'css-loader', 'less-loader']
        }
    ]
}

  如下图所示,head标签下面的三个样式是分别在.css和.less文件中定义的样式,而<head>标签上面的一个样式是在模版html中自己设定的。

CSS3为兼容自动加浏览器前缀(postcss-loader)

  用一个autoprefixer包和一个postcss-loader自动添加浏览器前缀,且这个插件是会更新的,以前transform在需要加上webkit前缀,但Chrome支持后postcss-loader就不会再给这个属性加上前缀了。

npm i postcss-loader autoprefixer -D
//在根目录创建postcss.config.js文件
module.exports = {
    plugins: [require('autoprefixer')]
}
//在rules css配置中新加入postcss-loader 
module: {
    rules: [ 
        //loader的用法。字符串只用一个loader 多个loader需要用数组 loader的顺序 默认从右向左 从下往上执行
        {
            test: /\.css$/,
            use: [{
                loader: 'style-loader',
                options: {
                    insertAt: 'top'//插在最上面,让自己写在模版html<style>标签中的样式优先级较高
                }
            }, 'css-loader']
        }
    ]
}

Plugin

  Plugin可以在webpack运行到某个阶段的时候,帮助我们做某些事情,类似于生命周期的概念。在某个时间点,需要某个机制完成一些事情。

生成模版html(HtmlWebpackPlugin插件)

  在我们打包文件后,该插件会生成一个html模版,并且把打包后的其他文件在该模版中引用。生成的html模版的内容是我们可以自己定义的。

const HtmlWebpackPlugin = require('html-webpack-plugin');  
plugins: [
    new HtmlWebpackPlugin({
        template: './src/index.html', //模版文件的位置
        filename: 'index.html', //打包出来html文件的名称
        minify: {
            removeAttributeQuotes: true, // 去除双引号
            collapseWhitespace: true, //变成一行
        },
        hash: true //添加一个hash戳
    })
],
抽离CSS(MiniCssExtractPlugin插件)

  我们打包时,把所有样式抽离出来生成一个CSS文件,在模版html文件中以link形式引入。

npm i mini-css-extract-plugin -D
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
plugins: [
    new MiniCssExtractPlugin({
        filename: 'main.css'
    })
]
module: {
    rules: [ 
        {
            test: /\.css$/,
            use: [
            MiniCssExtractPlugin.loader, //把style-loader换成MiniCssExtractPlugin.loader
            'css-loader', 
            'postcss-loader'
            ]
        },
        {
            test: /\.less$/,
            use: [
            MiniCssExtractPlugin.loader, 
            'css-loader', 
            'less-loader', 
            'postcss-loader'
            ]
        }
    ]
}
压缩CSS和JS(OptimizeCSSAssetsPlugin插件、UglifyJsPlugin插件)

  进行到这一步会发现,生产模式下打包出来的main.css也没有被压缩,是因为用了MiniCssExtractPlugin这个插件不会压缩css,需要自己压缩。使用OptimizeCSSAssetsPlugin插件配置优化项,但是使用这个插件之后,css确实压缩了,但js又不会压缩了,因此还要用到UglifyJsPlugin再来压缩js。

npm i optimize-css-assets-webpack-plugin -D
npm i uglifyjs-webpack-plugin -D  
const UglifyJsPlugin = require("uglifyjs-webpack-plugin"); //压缩js
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin"); //压缩css
optimization: {
    minimizer: [
        new UglifyJsPlugin({
            cache: true, //缓存
            parallel: true, //并发压缩
            sourceMap: true // set to true if you want JS source maps
        }),
        new OptimizeCSSAssetsPlugin({})
    ]
}
更新打包目录(CleanWebpackPlugin插件)

  在没有使用该插件之前,每次打包上一版本的文件会遗留在dist文件夹下,需要我们手动删除。这个插件,在每次打包之前,先把之前的dist文件夹删除,打包生成新的dist目录。

npm install --save-dev clean-webpack-plugin
const {CleanWebpackPulgin} = require('clean-webpack-plugin')  
plugins: [
    new CleanWebpackPlugin()
]

SourceMap

  源代码与打包后的代码的映射关系,帮助我们定位错误在源代码中的位置。在devtool字段中配置,推荐的配置如下:

  • cheap :较快,只定位行,不定位列。
  • moudle :定位引入的第三方模块的错误。
  • eval :速度最快,包裹模块代码。
  • source-map :生成map,内容是源代码和打包后的代码的映射。
devtool:"cheap-module-eval-source-map" //开发环境
devtool:"cheap-module-source-map" //线上生产环境

webpack-dev-server

  一个提升开发效率的利器,每次改完代码都要重新打包一次,刷新浏览器非常的麻烦。用webpack-dev-server搭建一个服务器,使得我们不用真实的打包,而是在内存中打包,放置到devSever服务器上,以便我们在开发时调试测试整个项目。
  下载webpack-dev-server:

npm i webpack-dev-server -D  

  之后,先在package.json中配置scripts,"dev" : "webpack-dev-server --config webpack.config.my.js"。然后配置一下devServer中的一些配置项:

devServer: { //开发服务器的配置
    port: 8889, //端口号
    progress: true, //进度条
    contentBase: './dist', //指定了服务器资源的根目录,但是在开发过程不会真实打包
    compress: true, //启用 gzip 压缩
    open: true //自动打开浏览器
},
mock

  联调期间,前后端分离,直接获取数据会跨域,上线后我们使⽤用nginx转发,开发期间,webpack-dev-server就可以搞定这件事。
  我们先启动服务器,mock一个接口:

const koa = require('koa')
const app = new koa()  

const Router = require('koa-router')
const router = new Router()  

router.get('/api/info', async (ctx, next) => {
   ctx.body = {
       username: 'zhunny',
       message: 'hello mock'
   }
   ctx.status = 200
})

app.use(router.routes())

app.listen(3000)

  此时在我们前端项目中请求该接口的数据,会存在跨域问题,我们在dev-server中配置服务器代理:

axios.get('http://localhost:3000/api/info').then(res=>{
    console.log(res)
})
devServer: { //开发服务器的配置
    port: 8889, //端口号
    progress: true, //进度条
    contentBase: './dist', //指定了服务器资源的根目录,但是在开发过程不会真实打包
    compress: true, //启用 gzip 压缩
    open: true, //自动打开浏览器
    proxy: {
       "/api": {
         target: "http://localhost:3000"
        }
    }
},

  之后修改请求的接口:

axios.get('/api/info').then(res=>{
    console.log(res)
})

转换es6语法及校验

  webpack本身可以处理ES6语法,但是有些浏览器对es6、es7或者是es8的语法还不能识别。出于兼容性的考虑,我们会使用Babel来将ES6转换成ES5语法。

  • babel-loader: 是Babel于webpack通信的桥梁。
  • @babel/core: Babel工具的核心代码库。
  • @babel/preset-env: ES6语法的转换规则。
npm i babel-loader @babel/core @babel/preset-env -D
{
    test: /\.js$/,
    exclude: /node_modules/, //排除该文件夹下的内容
    use: {
        loader: 'babel-loader',
        options: {
            presets: [
                '@babel/preset-env'
            ]
        }
    }
}
@babel/polyfill

  当然只配置上述字段是不够的,到此步为止,一些es6、7、8新增的方法和类依然不能被识别。我们还需要下载@babel/polyfill,它将es6、7、8中的语法特性打包放到浏览器中,相当于一个补丁包。
  它的基本使用方法是在入口js文件中引用:import '@babel/polyfill'。但是这种方法是引入整个补丁包,使得webpack打包后的体积变大。我们可以对这点进行优化。移除在js文件中引用的@babel/polyfill,配置文件中添加useBuiltIns字段,对使用到的es6、7、8语法特性按需加载。

presets: [
    [
        '@babel/preset-env',
        {
            useBuiltIns: 'usage',//按需加载
            corejs: 2 //指定core的版本
        }
    ]
],
@babel/plugin-transform-runtime

  当我们开发组件库、工具库这些场景时,在js文件中引用@babel/polyfill就不合适了。因为@babel/polyfill以全局变量的方式注入,会造成全局污染。上面用的按需加载usage的方法也不会造成全局污染,但是这个字段还在试验阶段。我们可以使用闭包方式@babel/plugin-transform-runtime来代替。但是这种方式就不会对原型链上的某些方法进行转义,因此开发正常的业务场景就比较适合用polyfill,无所谓全局变量的影响,我们不需要担心某些原型链上的方法没有被转义。

npm i @babel/plugin-transform-runtime -D
npm i @babel/runtime -S  
{
    test: /\.js$/,
    exclude: /node_modules/, //排除该文件夹下的内容
    use: {
        loader: 'babel-loader',
        options: {
            plugins: [
                [
                '@babel/plugin-transform-runtime',
                {
                    absoluteRuntime:false,
                    corejs:2,
                    helpers:true,
                    regenerator:true,
                    useESMoudules:false
                }
                ]
            ]
        }
    }
}
.babelrc

  Babel配置可能内容较多,我们可以把options内容放到.babelrc中。

//.babelrc
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage", //按需加载 实验性的功能
        "corejs": 2
      }
    ],
    "@babel/preset-react"
  ]
}
一些提案语法的补丁包

  一些es7的提案如class,则还需要用到@babel/plugin-proposal-class-properties,装饰器则需要用到@babel/plugin-proposal-decorators:

npm i @babel/plugin-proposal-class-properties -D
npm i @babel/plugin-proposal-decorators -D

{
    test: /\.js$/,
    use: {
        loader: 'babel-loader',
        options: {
            presets: [
                '@babel/preset-env'
            ],
            plugins: [
                ['@babel/plugin-proposal-decorators', { "legacy": true }],
                ['@babel/plugin-proposal-class-properties', { "loose": true }],
                ['@babel/plugin-transform-runtime'] //generator
            ]
        }
    }
}
校验

  js语法的校验用到了eslint以及eslint-loader,他的官网为https://eslint.org,eslint在使用时需要配置一个.eslint.json的规则文件放在根目录,具体配置项见官网。

npm i eslint eslint-loader -D
{
    test: /\.js$/,
    use: {
        loader: 'eslint-loader',
        options: {
            enforce: 'pre' //在普通loader之前执行
        }
    },
}

全局变量的引入

  引入全局变量有三种方式,假如要在全局引入jquery库:

  1. 使用内联loader expose-loader将jquery暴露到window属性上。
import $ from 'jquery'
require(expose-loader)
console.log(window.$)
  1. 使用webpack.providePlugin插件将$注入到每个模块。
new webpack.providePlugin(
    {$:'jquery'}
)
  1. 引入jquery的cdn。

参考

webpack官方网站
Babel官方网站