webpack配置整理

自从node出现带来的前端大爆炸时代以来,前端工程化构建相关的工具层出不穷,如fis、gulp、grunt、webpack等,但如果让我说最全面的构建工具,那非webpack莫属了。

"webpack一统天下!"

最近看掘金小册中有一篇关于webpack的配置,从头撸了一遍感觉收获颇丰,所以实践、整理、记录一下整个过程相关学习。因为眼下webpack4.*出现,所以此次整理以4.*为主,在整理过程中会尽可能理清3.*4.*的区别,以求更清晰的分析和理解。

※※※ : 新建文件
$$$ : 新依赖

安装和环境区分

一般来说我们更多的将webpack作为项目开发依赖来安装使用,并且固定版本,有利于多人协作开发。
项目init,保证项目有package.json文件 。※※※

npm init

安装webpackwebpack-cli,并作为项目开发依赖(此处默认使用webpack4.8.1版本)$$$

npm install webpack webpack-cli -D

接下来,我们需要在package.json中添加相关script。

  1. 此处的—mode参数,是webpack4.*不可缺少的参数,用于区分当前环境,关于环境区分后续会讲。
  2. webpack-dev-server模块需要安装,在本地开启一个简单的静态服务来进行开发。$$$
npm install webpack-dev-server -D 
"scripts": {
    "build": "webpack --mode production",
    "start": "webpack-dev-server --mode development"
},

由于webpack 4.x 的版本可以零配置就开始进行构建,但是为了更定制化,我们还需要自己配置自己的config文件。接下来我们来新建和配置自己的webpack.config.js文件。※※※
webpack的构建离不开一个入口文件,webpack会读取这个文件,从他开始解析依赖,打包等操作。所以我们需要新建一个入口文件entry.js文件。※※※
webpack配置最终export一个对象或者一个函数,此处我们使用函数形式。

module.exports= {}

// 或者 webpack 4.0 暴露一个函数,可以获取到mode的环境变量
module.export = (env, argv) => {
    console.log(env, argv.mode)
    return {
        entry: {
            index: './entry.js'
        }
    }
}

此时我们可以试着启动项目,来查看打印出来的mode是否是对应的development和production。

npm start   // => undefined development
npm run build   // => undefined production

区分环境除了用webpack4.*—mode参数,还可以用如下script,还可以将两者集合:

"scripts": {
    "build_": "NODE_ENV=production webpack",
    "start_": "NODE_ENV=development webpack-dev-server"
},

webpack.config.js中可以打印process.env.NODE_ENV查看

// webpack.config.js
console.log('process.env.NODE_ENV =====>', process.env.NODE_ENV)

npm run start_  // => development
npm run build_  // => production

环境区分之后,我们就可以实现dev和prd不同的配置,以达到我们想要的效果。

常用配置

我们回到webpack的配置中来,webpack配置常用的主要字段为:

  • entry 入口
  • output 出口
  • resolve 处理依赖模块路径的解析
  • module 处理多种文件格式的loader
  • plugins 除了文件格式转化由loader来处理,其他大多数由plugin来处理
  • devServer 配置 webpack-dev-server
  • optimization 优化,如4.*的chunk分离等
  • ...

entry

如下代码中, indexvendor 是多入口名称,后续打包过程会有用!项目可以单入口也可以多入口。
vendor指向node_module中的vue组件,是为了后续chunk分离出单独的js文件,因为不常修改,可以单独打包而且可以使用chunkhash来维持相同的hash值,保证用户最大程度利用浏览器缓存机制。

{
    entry: {
        vendor: ["vue"],
        index: './src/index.js'
    }
}

output

如下代码中,[name]是根据entry中的多入口的名字。此处还可以获取[hash]、[chunkhash]等,用于相应的需求。代码中?rd=[hash] 是为了配合接下来的html-webpack-plugin,可以将js插入html中并加上随机数,实现清缓存功能。
[chunkhash]可以保证当chunk的部分不变化情况下,不变化hash值,实现最大程度利用缓存机制。
注意:后续热更新(HMR)不能和[chunkhash]同时使用。 解决:如果是开发环境,将配置文件中的chunkhash 替换为hash

{
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js?rd=[hash:8]'
    }
}

resolve

webpack 中有一个很关键的内部模块 enhanced-resolve 就是处理依赖模块路径的解析的。
modules:指定了全局的node_modules,为了避免在内部一层层查找node_module文件夹,优化性能:
extensions:指定了一个可以省略后缀名的文件类型数组,一般不推荐使用太多,有查找性能问题。
alias:指定了访问路径的别名,可以在项目中直接访问如:import "css/a.less"

resolve: {
    modules: [
        // 使用绝对路径指定项目 node_modules,不做过多(一层层)查询
        path.resolve(__dirname, 'node_modules'), 
    ],
    extensions: ['.vue', '.js', '.json', '.jsx', '.css'],
    alias: {
        'css': path.resolve(__dirname, 'src/assets/css')
    }
},

module

webpack 中提供一种处理多种文件格式的机制,便是使用 loader。我们可以把 loader 理解为是一个转换器,负责把某种文件格式的内容转换成 webpack 可以支持打包的模块。
这里举例常用的几个loader,更多的loader可以查看:webpack loader
rules是一个数组,包括多个loader
test: 匹配文件路径的正则表达式,通常我们都是匹配文件类型后缀
include: 指定哪些路径下的文件需要经过 loader 处理,node_module不需要处理,性能问题
loader: 单个loader可以直接用这个字段,否者可以用use字段,这些loader一般都需要安装依赖 $$$
use:指定使用的loader,use字段是一个数组,注意顺序问题,先使用的loader需要排在后面,ExtractTextPlugin是模块extract-text-webpack-plugin用于将文件分离,这个plugin比较特殊需要loader的配合,后续讲解。

// 简单的loader
module: {
    rules: [{
        test: /\.(less|css)$/,
        include: [
            path.resolve(__dirname, 'src')
        ],
        use: [
            { loader: 'css-loader'},
            { loader: 'style-loader'}
        ]
    }]
}

// 如果结合ExtractTextPlugin插件分离文件和postcss-loader等其他loader
module: {
    rules: [{
        test: /\.(less|css)$/,
        include: [
            path.resolve(__dirname, 'src')
        ],
        use: ExtractTextPlugin.extract({
            use: [{
                // 负责解析 CSS 代码,主要是为了处理 CSS 中的依赖,
                // 例如 @import 和 url() 等引用外部文件的声明
                loader: 'css-loader',
                options: {
                    minimize: true, // 使用 css 的压缩功能
                },
            },
                  { loader: 'postcss-loader'},
                  { loader: 'less-loader' }
                 ],
            // 会将 css-loader 解析的结果转变成 JS 代码,
            // 运行时【动态】插入 style 标签来让 CSS 代码生效
            fallback: 'style-loader'
        })
    }]
}

常用的loader还有:

url-loader,url-loader 和 file-loader 的功能类似,但前者可以转成base64 $$$
image-webpack-loader,image-webpack-loader 的压缩是使用 imagemin 提供的一系列图片压缩类库来处理的 $$$
babel-loader,这个不多说了,es6离不开babel。 $$$

plugins

模块代码转换的工作由 loader 来处理,除此之外的其他任何工作都可以交由 plugin 来完成,plugin分为webpack内置和第三方的。
CopyWebpackPluginHtmlWebpackPluginExtractTextPlugin都是非常重要的plugin。具体看代码中的注释:$$$

const UglifyPlugin = require('uglifyjs-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

plugins: [
        new webpack.DefinePlugin({
            'process.env': {
                NODE_ENV: JSON.stringify(process.env.NODE_ENV) 
            }
        }),
        // 直接将static文件复制到dist中
        new CopyWebpackPlugin([
            { from: 'static/*.*', to: '', } 
        ]),
        // webpack 4.x 版本运行时,mode 为 production 即会启动压缩 JS 代码的插件,
        // 屏蔽此插件,3.x可以用这个插件
        new UglifyPlugin(),
        // 如果我们的文件名或者路径会变化,例如使用 [hash] 来进行命名,那么最好是将 HTML 引用路径和我们的构建结果关联起来,这个时候我们可以使用 html-webpack-plugin。
        // 如果需要添加多个页面关联,那么实例化多个 html-webpack-plugin, 并将它们都放到 plugins 字段数组中就可以了。
        new HtmlWebpackPlugin({
            filename: 'index.html', // 配置输出文件名和路径
            template: './index.html',    // 配置html文件模板,将js和这个关联起来
            minify: { // 压缩 HTML 的配置
                minifyCSS: true, // 压缩 HTML 中出现的 CSS 代码
                minifyJS: true, // 压缩 HTML 中出现的 JS 代码
                removeComments: true,   // 移除注释
                collapseWhitespace: true,   // 缩去空格
                removeAttributeQuotes: true // 移除属性引号
            }
        }),
        // 使用方式特别,除了在plugins字段添加插件实例之外,还需要调整 loader 对应的配置。
        // 配置输出的文件名,这里同样可以使用 [hash],多个文件会加载在一起
        // name 是根据entry中的入口名字
        new ExtractTextPlugin({
            filename: '[name].css?rd=[hash:8]'
        }),
        // 在 HMR 更新的浏览器控制台中打印更易读的模块名称
        new webpack.NamedModulesPlugin(),   
        // Hot Module Replacement 的插件
        //【在这个概念出来之前,我们使用过 Hot Reloading,当代码变更时通知浏览器刷新页面】
        //【HMR 可以理解为增强版的 Hot Reloading,不用整个页面刷新,而局部替换掉模块】
        new webpack.HotModuleReplacementPlugin()
    ],

devServer

在 webpack 的配置中,可以通过 devServer 字段来配置 webpack-dev-server,如端口设置、启动 gzip 压缩等,这里简单讲解几个常用的配置。
一般来说只要使用hot: true ,来实现模块热替换。

devServer: {
    // public: 'http://localhost:8080/',   // public 字段用于指定静态服务的域名
    // publicPath: 'static/',      // 字段用于指定构建好的静态文件在浏览器中用什么路径去访问
    port: '1234',
    hot: true,  // 模块热替换
    before(app) {
        // 当访问 /some/path 路径时,返回自定义的 json 数据
        // 可以用于拦截部分请求返回特定内容,或者实现简单的数据 mock。
        app.get('/api/test.json', function (req, res) { 
            res.json({ code: 200, message: 'hello world' })
        })
    },
    /* proxy: {
        '/login': {
            target: 'http://127.0.0.1:8090',
            changeOrigin: true,
            pathRewrite: {
                '^/login': '/login'
            }
        }
    } */
},

optimization

webpack配置优化,比如webpack4.* chunks分离就移到这里很简单的设置:
在webpack3.*中,想实现chunks分离需要用到webpack.optimize.CommonsChunkPlugin插件:

plugins: [
    new webpack.optimize.CommonsChunkPlugin({
        name: 'vendor',         // 使用 vendor 入口作为公共部分
        filename: "vendor.js",  
        // minChunks: 3,        // 公共的部分必须被 3 个 chunk 共享
        minChunks: Infinity,    // 这个配置会让webpack不再自动抽离公共模块,不管超过多少都不抽离,只抽离指定的vendor
    })   
]

在webpack4.*中,分离chunks移到了optimization中:

optimization: {
    // webpack4.* chunks分离
    splitChunks: {
        // chunks: "all", // 所有的 chunks 代码公共的部分分离出来成为一个单独的文件
        cacheGroups: {
            vendor: {
                chunks: "initial",
                test: "vendor",
                name: "vendor", // 使用 vendor 入口作为公共部分
                enforce: true,
            },
        }
    }
}

中间件

熟悉Node的都知道中间件,简单的说是来完成某件事情的插件。比如webpack-dev-middleware就是在 Express 中提供 webpack-dev-server 静态服务能力的一个中间件 【dev】 $$$

npm install webpack-dev-middleware express -D

接下来我们创建一个dev.js,然后我们用node dev.js来执行,dev.js代码如下:

const webpack = require('webpack')
const chalk = require('chalk')      // 命令行中彩色出书
const opn = require('opn')          // 自动打开浏览器
const ora = require('ora')          // loading样式
const midddleware = require('webpack-dev-middleware')   
const webpackOptions = require('./webpack.config.js')

const spinner = ora({
    text: chalk.magenta('> 加载中...'),
    spinner: 'bouncingBar'
}).start()
const port = 3000

// 本地的开发环境,可以再次全局设置mode,用于webpack配置中获取。如果script中设置了,就不需要这里设置了。
webpackOptions.mode = 'development'
const compiler = webpack(webpackOptions)

const express  = require('express')
const app = express()

let hotMidddleware = midddleware(compiler, {
    // webpack-dev-middleware 的配置选项
})
app.use(hotMidddleware)

// 编译完成的回调函数
hotMidddleware.waitUntilValid(() => {
    spinner.stop()
    console.log(chalk.magenta('> 编译完成...'))
    console.log(chalk.magenta(`> 监听:http://localhost:${port}\n`))
    opn(`http://localhost:${port}`)
})

app.listen(port, () => {
    console.log(chalk.magenta('> 项目运行...'))
})

有了这样的中间件,我们完全可以创建一套cli,区分开发、测试、生产打包的配置。具体可以尝试实践,回头看vue-cli的配置,大体上没什么大问题了。

总结

env相关总结:

webpack 3:

​ package.json中 "start_": "NODE_ENV=development webpack-dev-server"

  1. 在webpack.config.js中可以获取 process.env.NODE_ENV
  2. 在runtime项目代码中【不能】获取到 process.env.NODE_ENV,默认为production。要想在项目中获取,需要webpack.DefinePlugin这个plugin设置

webpack 4:

​ package.json中 "start": "webpack-dev-server --mode development"

  1. 在webpack.config.js中【不能】获取process.env.NODE_ENV,需要把配置作为函数返回值暴露 module.exports = (env, argv) => {},其中arv.mode可以获取
  2. 在runtime项目代码中可以获取到 process.env.NODE_ENV。(注意不要设置webpack.DefinePlugin)
  3. 更加快速的增量编译构建。(hot reload比 webpack 3比较快)

我觉得可以集合两者,定义script中为"start": "NODE_ENV=development webpack-dev-server --mode development"

postcss

这里只说其中一种方法:

  1. 安装依赖postcss-loader以及其他的postcss的plugin, $$$

    npm install postcss-loader autoprefixer -D
    
  2. 根目录目录建 .postcssrc.js

    module.exports = {
        "plugins": {
            // to edit target browsers: use "browserlist" field in package.json
            "autoprefixer": {}
        }
    }
    
  3. package.json中添加browserslist

    "browserslist": [
        "defaults",
        "not ie < 8",
        "last 2 versions",
        "> 1%",
        "iOS 7",
        "last 3 iOS versions"
      ]
    
  4. webpack.config.js的匹配css的loader中加上postcss-loader

懒加载

遵循 ES 标准的动态加载语法 dynamic-import 来编写,如果你使用了 Babel 的话,还需要 Syntax Dynamic Import 这个 Babel 插件来处理 import() 这种语法。

项目代码中:

setTimeout(() => {  // 模拟异步
    // 注释指定了chunk名
    import(/* webpackChunkName: "lodash_" */ './lodash.js').then((res) => {
        console.log(res)
    }) 
}, 5000);

安装依赖"babel-plugin-syntax-dynamic-import"

npm install babel-plugin-syntax-dynamic-import -D

项目根目录创建.babelrc文件

{   
    "presets": [["env", { "modules": false }]],
    "plugins": [
        "syntax-dynamic-import"
    ]
}

webpack.config.js中首先要有laoder,再者output中添加chunkFilename,为了分离出来有对应的名字:

output: {
    path: path.resolve(__dirname, 'dist'),
    chunkFilename: '[name].js?rd=[hash:5]'
},
module: {
        rules: [{
            test: /\.jsx?/, // 支持 js 和 jsx;x可有可无
            include: [
                // src 目录下的才需要经过 babel-loader 处理
                path.resolve(__dirname, 'src')
            ],
            loader: 'babel-loader',
        }]
}

你可能感兴趣的:(webpack配置整理)