webpack由浅入深——(webpack基础配置)

5,237

webpack的作用

构建就是把源代码转换成发布到线上的可执行JavaScrip、CSS、HTML代码,包括如下内容:

  • 代码校验:在代码被提交到仓库前需要校验代码是否符合规范,以及单元测试是否通过。
  • 代码转换:TypeScript 编译成 JavaScript、SCSS 编译成 CSS 等。
  • 模块合并:在采用模块化的项目里会有很多个模块和文件,需要构建功能把模块分类合并成一个文件。
  • 代码分割:提取多个页面的公共代码、提取首屏不需要执行部分的代码让其异步加载。
  • 文件优化:压缩 JavaScript、CSS、HTML 代码,压缩合并图片等。
  • 自动刷新:监听本地源代码的变化,自动重新构建、刷新浏览器。
  • 自动发布:更新完代码后,自动构建出线上发布代码并传输给发布系统。

webapck核心概念

  • Entry:入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。
  • Output:输出结果,在Webpack经过一系列处理并得出最终想要的代码后输出结果。
  • Module:模块,在 Webpack里一切皆模块,一个模块对应着一个文件。Webpack会从配置的 Entry 开始递归找出所有依赖的模块。
  • Chunk:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。
  • Loader:模块转换器,用于把模块原内容按照需求转换成新内容。
  • Plugin:扩展插件,在Webpack构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要的事情。

webpack工作过程

  1. Webpack 启动后会从Entry里配置的Module开始递归解析 Entry 依赖的所有 Module。
  2. 每找到一个 Module, 就会根据配置的Loader去找出对应的转换规则,对Module 进行转换后,再解析出当前 Module 依赖的 Module。 这些模块会以 Entry 为单位进行分组,一个 Entry 和其所有依赖的 Module 被分到一个组也就是一个 Chunk。
  3. 最后 Webpack 会把所有 Chunk 转换成文件输出。 在整个流程中 Webpack 会在恰当的时机执行Plugin 里定义的逻辑。

webpack系列文章

  1. webpack由浅入深——(webpack基础配置)
  2. webpack由浅入深——(webpack优化配置)
  3. webpack由浅入深——(tapable)
  4. webpack由浅入深——(webapck简易版)
  5. webpack由浅入深——(ast、loader和plugin)

初始化项目

首先确保电脑已经安装了nodejs,推荐采用nvm的形式进行安装,这样就不用配置环境变量或者创建软链接。

mkdir webpack-learn //通过命令行创建文件夹
cd webpack-learn    //打开创建的文件夹
npm init -y //初始化一个项目
npm install webpack webpack-cli -D //本地安装webpack和webpack-cli
mkdir src   //创建src目录来存放源代码
mkdir dist  //创建dist目录来存放打包后的代码

在src目录下创建index.js

let str = require('./a');
console.log(str);

在src目录下创建a.js

module.exports='webpack-learn'

在dist目录下创建index.html文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<div id="root"></div>
<script src="bundle.js"></script>
</body>
</html>

利用webpack进行打包,关于commonjs不清楚的,请参考require()源码解读

npx webpack -- mode development //以默认以开发模式进行打包

mode-d

Entry(入口)和Output(输出)

从上面的代码中可以看到文件打包到了dist/main.js中,所以需要进行相应的配置webpack.config.js来自定义结果文件名。

单入口

  • 字符串形式
const path=require('path');
module.exports={
    entry: './src/index.js',    //入口
    output: {
        path: path.resolve(__dirname,'dist'),   //出口,绝对路径
        filename:'bundle.js'
    },
    module: {},     //配置loader
    plugins: [],    //配置插件
    devServer: {}   //配置本地服务
    resolve:{},     //配置解析文件路径等
}
  • 数组形式
//index.js
console.log('hello');
//a.js
console.log('world')
const path=require('path');
module.exports={
    //index和a之间没有依赖关系,只是单纯的合并
    entry: ['./src/index.js','./src/a.js'],
    output: {
        path: path.resolve(__dirname,'dist'),  
        filename:'bundle.js'
    },
    module: {},     
    plugins: [],    
    devServer: {}   
    resolve:{},
}

多入口

//a.js
let str =require('./c')
console.log(str);
//b.js
let str =require('./d')
console.log(str);
//c.js
module.export = 'hello'
//d.js
module.export = 'world'
const path=require('path');
module.exports={
    //多入口拆分功能,可以两个页面分别引用一个,也可以一个页面引用多个
    //配合后面的html-webpack-plugin使用
    entry: {
        pageA:'./src/a',
        pageB:'./src/b'
    },
    output: {
        path: path.resolve(__dirname,'dist'), 
        //带有哈希值的文件名exp:pageA.fa112c62、pageB.fa112c62
        filename:'[name].[hash:8].js'    
    },
    module: {},     
    plugins: [],    
    devServer: {}   
    resolve:{},
}

自动生成页面

现在页面是手动创建到dist目录下的,一个页面还好,如果存在多个页面,手动创造html的代价是很大的,可以利用html-webpack-plugin来自动创建页面:

npm install html-webpack-plugin -D
//src/index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
</body>
</html>
const path = require('path');
const webpack =require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    entry:{
        pageA:'./src/a',
        pageB:'./src/b'
    },
    output: {
        path: path.resolve(__dirname,'dist'), 
        filename:'[name].[hash:8].js'    
    },
    module: {},
    plugins: [
        //在实际项目中,通过读取需要创建的页面信息,遍历创建实例
         new HtmlWebpackPlugin({
            template:'./src/index.html',
            filename:'pageA.html',
            chunks:['pageA,pageB'],   //数组,可以放多个chunk
            //页面资源携带哈希值exp:pageA.fa112c62?r2452567&4124
            //中间哈希一直都有,这个后面的哈希只在页面引用添加在页面中
            hash:true,  
            minify:{
                collapseWhitespace:true, //压缩代码,去除空格和换行
                removeAttributeQuotes:true//压缩代码,去除属性双引号    
            }
        }),
        new HtmlWebpackPlugin({
            template:'./src/index.html',
            filename:'pageB.html',
            chunks:['pageB'],
            hash:true,
            minify:{
                collapseWhitespace:true,
                removeAttributeQuotes:true
            }
        })
    ],
    devServer: {},
    resolve:{},
}

配置script

由于每次打包都需要执行npx webpack --mode development,所以可以在package.json中进行配置:

"scripts": {
    //等价于webpack --config webpack.config.js --mode development
    //默认是执行webpack.config.js,可以根据env来配置执行不同的文件
    "start": "webpack --mode development"
  },

devServer(本地服务器)

首先必须的安装插件webpack-dev-derver:

npm install webpack-dev-server -D   

修改script:

"scripts": {
    "dev":"webpack-dev-server --mode development"
    "build": "webpack --mode development"
 },

修改配置文件:

//index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div id='app'></div>
</body>
</html>
//a.js
module.exports = 'hello'
//index.js
let str = require('./a');
document.getElementById('app').innerHTML = str;
const path=require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports={
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname,'dist'), 
        filename:'bundle.js'
    },
    module: {}, 
    plugins: [
         new HtmlWebpackPlugin({
            template:'./src/index.html',
            filename:'index.html',
            hash:true,
            minify:{
                collapseWhitespace:true,
                removeAttributeQuotes:true
            }
        })
    ],
    devServer: {
        //devServer会在打包的时候把生成的文件放在内存中,
        //并且是以根目录为参照目录而不是dist目录,所以需要修改
        contentBase:'./dist',
        port:'3000',
    } 
    resolve:{},
}

通过浏览器,输入localhost:3000就可以看到

本地服务
通过修改a.js中module.exports = 'hello1',可以看到
热更新
这样会有一个问题,就是只要源代码中存在改动,就会刷新页面。假如在react本地开发的时候,有很多组件,其中一个组件的代码修改了,不希望所有的组件都更新,可以利用热更新来解决:

const path = require('path');
const webpack =require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports={
    entry:'./src/index.js',
    output: {
        filename:'bundle.js',
        path: path.resolve(__dirname,'dist')
    },
    module: {},
    plugins: [
        //使用热更新插件
        new webpack.HotModuleReplacementPlugin();
        new HtmlWebpackPlugin({
            template:'./src/index.html',
            filename:'index.html',
            hash:true,
            minify:{
                collapseWhitespace:true,
                removeAttributeQuotes:true
            }
        })
    ],
    devServer: {    
        contentBase:'./dist',
        port:'3000',
        hot:true    //热更新开关,使用websocket来通知页面更新
    },
    resolve:{},
}
//index.js
let str = require('./a');
document.getElementById('app').innerHTML = str;

//这里必须加这段,不然的话,还是没有办法使用热更新
if(module.hot){
    module.hot.accept();
}

配置proxy

 devServer: {    
        contentBase:'./dist',
        port:'3000',
        hot:true,
        proxy:{
            target:'http://xxxxx'   //代理的服务器
            pathRewirte:{
                '/xxx':'/yyy'
            }
        }
    },
  • webpack服务集成到自己的本地服务
// 把webpack-dev-server的配置引入到自己的本地服务
const express = require('express');
const webpackDevMiddleware = require('webpack-dev-middleware');
// 引入webpack配置文件
const config = require('./webpack.config');
let app = express();

cosnt webpack = require('webpack');
let compiler = webpack(config);     //用webpack进行编译
app.use(webpackDevMiddleware(compiler)); 

app.listen(3000);

module中的loader

webpack每找到一个Module,就会根据配置的Loader去找出对应的转换规则,让js能过编译css、ejs、jsx和图片的各种格式。

css相关loader

  • style-loader 和css-loader
//index.css
body{
    background:red;
    border-radio:4px;
}
//一般前端代码使用import引入模块,node服务使用require引入模块
import str from './a'
import './index.css'
document.getElementById('app').innerHTML=str

if(module.hot){
    module.hot.accept();
}

执行npm run build之后发现出现以下报错,说明需要配置loader:

loader报错
首先需要安装loader

npm install css-loader style-loader -D
const path = require('path');
const webpack =require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    entry:{
        index:'./src/index'
    },
    output: {
        filename:'[name].[hash:8].js',
        path: path.resolve(__dirname,'dist')
    },
    module: {
        rules:[
            {
                test:/\.css$/,
                //多个loaders用数组,loader的形式有字符串和对象两种
                //字符串形式:'xxx?option1!yyyy'
                //对象形式{loader:'xxx',options:{option1:yyyy}}
                //less-laoder将less转化为css,css-loader解析css,style-loader插入到style标签中
                use:['style-oader','css-loader']    
            }
        ]
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin(),
        new HtmlWebpackPlugin({
            template:'./src/index.html',
            filename:'index.html',
            chunks:['index']
        })
    ],
    devServer: {    
        contentBase:'./dist',
        port:'3000',
        hot:true
    },
    resolve:{},
}

用npm run dev启动服务可以看到效果,但是这样有一个问题,css样式采用的内嵌式,最好能抽离出来使用外链式引入,可以使用插件mini-css-extract-plugin:

npm install mini-css-extract-plugin -D
const path = require('path');
const webpack =require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
    entry:{
        index:'./src/index'
    },
    output: {
        filename:'[name].[hash:8].js',
        path: path.resolve(__dirname,'dist')
    },
    module: {
        rules:[
            {
                test:/\.css$/,
                use:[{
                    loader:MiniCssExtractPlugin.loader,
                    options:{
                        //将css中的路经前面添加,background:url('xxxx')
                        //http://ssss/xxxxx
                        publicPath:'http://sssss'    
                    }
                },
                'css-loader'
                ]    
            }
        ]
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin(),
        new MiniCssExtractPlugin({
            filename:index.css,
        }),
        new HtmlWebpackPlugin({
            template:'./src/index.html',
            filename:'index.html',
            chunks:['index']
        })
    ],
    devServer: {    
        contentBase:'./dist',
        port:'3000',
        hot:true
    },
    resolve:{},
}
  • postcss-loader和autoprefixer插件

由于前端写的代码要兼容各种浏览器,css属性为了兼容各大浏览器,往往需要添加各种前缀,但是同一个属性写多份,这个工作量还是比较大的,有一个postcss-loader可以配合autoprefixer插件使用

//index.css
body{
    background:red;
    transform: rotate(0,0,100deg);
}
npm install postcss-loader autoprefixer -D
//创建postcss.config.js
module.exports ={
    plugins:[
        require('autoprefixer')
    ]
} 
const path = require('path');
const webpack =require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
    entry:{
        index:'./src/index'
    },
    output: {
        filename:'[name].[hash:8].js',
        path: path.resolve(__dirname,'dist')
    },
    module: {
        rules:[
            {
                test:/\.css$/,
                use:[
                {loader:MiniCssExtractPlugin.loader},
                'css-loader',
                'postcss-loader'    //添加css前缀处理laoder
                ]    
            }
        ]
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin(),
        new MiniCssExtractPlugin({
            filename:index.css,
        }),
        new HtmlWebpackPlugin({
            template:'./src/index.html',
            filename:'index.html',
            chunks:['index']
        })
    ],
    devServer: {    
        contentBase:'./dist',
        port:'3000',
        hot:true
    },
    resolve:{},
}

图片相关的loader

项目中引入图片的方式有三种:

  1. 通过js引入
//index.js
import './index.css'
import jpg from './1.jpg'
let img = new Image();
img.src=jpg;
document.body.appendChild(img);
if(module.hot){
    module.hot.accept();
}
  1. 通过css引入
//index.css
body{
    background: url('./1.jpg') no-repeat right;
}
  1. 通过img的src属性引入
//index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <img src="./1.jpg" alt="">
</body>
</html>

处理前两种引入图片的方式需要使用file-loader和url-loader,其中url-laoder内部会引用file-loader,它们的作用就是解析js和css中的图片链接然后将图片变成base64。后一种引入图片的方式需要使用html-withimg-loader。

npm install file-loader url-loader html-withimg-loader -D
const path = require('path');
const webpack =require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
    entry:{
        index:'./src/index'
    },
    output: {
        filename:'[name].[hash:8].js',
        path: path.resolve(__dirname,'dist')
    },
    module: {
        rules:[
            {
                test:/\.css$/,
                use:[{
                    loader:MiniCssExtractPlugin.loader,
                },'css-loader']
            },
            {
                test:/\.jpg|png/,
                use:{
                    loader:'url-loader',
                    options:{
                        //大于8k的图片会打包到dist目录下,小于8k的图片会生成base64插入到引用的地方
                        //base64会使资源变大1/4,但是base64无需发送请求,资源比较小时使用base64最佳
                        limit:8*1024   
                    }
                }
            },
            {
                test:/\.html$/,
                use:'html-withimg-loader'
            }
        ]
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin(),
        new MiniCssExtractPlugin({
            filename:index.css,
        }),
        new HtmlWebpackPlugin({
            template:'./src/index.html',
            filename:'index.html',
            chunks:['index']
        })
    ],
    devServer: {    
        contentBase:'./dist',
        port:'3000',
        hot:true
    },
    resolve:{},
}

img-loader

js相关的loader

由于ES6的简洁和API扩展,有很多开发者使用ES6进行开发,但是由于浏览器的品牌和版本的不同,就出现了开发时使用ES6然后同意转化成ES5的情况。这时候就需要使用babel对ES6+的语法进行转译,关于babel的各种配置可以参考对babel-transform-runtime,babel-polyfill的一些理解

npm install babel-core babel-loader babel-preset-env babel-preset-stage-0 babel-plugin-tranform-runtime -D
//创建.babelrc文件
//preset中包含了一组用来转换ES6+的语法的插件,但是还不转换新的API
//如需使用新的API,例如set(),还需要使用对应的转换插件或者polyfill(填充库)
{
    presets:{
        'env',      //环境变量,根据不同浏览器环境而对应的转码
        'stage-0'   //转译ES6+(0 > 1 > 2 > 3 > 4)
    }
    plugins:{
        'tranform-runtime'
    }
}
const path = require('path');
const webpack =require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
    entry:{
        index:'./src/index'
    },
    output: {
        filename:'[name].[hash:8].js',
        path: path.resolve(__dirname,'dist')
    },
    module: {
        rules:[
            {
                test:/\.css$/,
                use:[{
                    loader:MiniCssExtractPlugin.loader,
                },'css-loader']
            },
            {
                test:/\.jpg|png/,
                use:{
                    loader:'url-loader',
                    options:{
                        limit:8*1024   
                    }
                }
            },
            {
                test:/\.html$/,
                use:'html-withimg-loader'
            },
            {   //使用babel-loader
                test:/\.js$/,
                use:'babel-loader',
                exclude:/node_modules/  //排除编译ndoe——modules   
            }
        ]
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin(),
        new MiniCssExtractPlugin({
            filename:index.css,
        }),
        new HtmlWebpackPlugin({
            template:'./src/index.html',
            filename:'index.html',
            chunks:['index']
        })
    ],
    devServer: {    
        contentBase:'./dist',
        port:'3000',
        hot:true
    },
    resolve:{},
}

Plugin(插件)

plugin和loader的区别在于loader只在编译module时执行,而plugin可能在webapck工作流程的各个阶段执行。

clean-webpack-plugin

清除dist目录

module.exports = {
    mode:'production',  生产环境会自动压缩   
    entry:{
        index:'./src/index'
    },
    output: {
        filename:'[name].[hash:8].js',
        path: path.resolve(__dirname,'dist')
    },
    module: {
        rules:[
            {
                test:/\.css$/,
                use:[{
                    loader:MiniCssExtractPlugin.loader,
                },'css-loader']
            },
            {
                test:/\.jpg|png/,
                use:{
                    loader:'url-loader',
                    options:{
                        limit:8*1024   
                    }
                }
            },
            {
                test:/\.html$/,
                use:'html-withimg-loader'
            },
            {   
                test:/\.js$/,
                use:'babel-loader',
                exclude:'/node_modules/'
            }
        ]
    },
    plugins: [
        new CleanWebpackPlugin(['dist/*.*']),   //清空dist目录
        new webpack.HotModuleReplacementPlugin(),
        new MiniCssExtractPlugin({
            filename:index.css,
        }),
        new HtmlWebpackPlugin({
            template:'./src/index.html',
            filename:'index.html',
            chunks:['index']
        })
    ],
    externals:{
        'jquery':'$'
    }
    devServer: {    
        contentBase:'./dist',
        port:'3000',
        hot:true
    },
    resolve:{},
}

uglifyjs-webpack-plugin和optimize-css-assets-webpack-plugin

module.exports = {
    mode:'production',  生产环境会自动压缩   
    entry:{
        index:'./src/index'
    },
    output: {
        filename:'[name].[hash:8].js',
        path: path.resolve(__dirname,'dist')
    },
    optimization:{
        minimizer:{
            new UglifyJSPlugin({    //压缩js
                cache:true,
                parallel:ture,  //并行压缩
                sourthMap:true, //启动sourthMap
            }) ,
            new OptimizeCssAssetsPlugin()   //压缩css
        }
    },
    module: {
        rules:[
            {
                test:/\.css$/,
                use:[{
                    loader:MiniCssExtractPlugin.loader,
                },'css-loader']
            },
            {
                test:/\.jpg|png/,
                use:{
                    loader:'url-loader',
                    options:{
                        limit:8*1024   
                    }
                }
            },
            {
                test:/\.html$/,
                use:'html-withimg-loader'
            },
            {   
                test:/\.js$/,
                use:'babel-loader',
                exclude:'/node_modules/'
            }
        ]
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin(),
        new MiniCssExtractPlugin({
            filename:index.css,
        }),
        new HtmlWebpackPlugin({
            template:'./src/index.html',
            filename:'index.html',
            chunks:['index']
        })
    ],
    externals:{
        'jquery':'$'
    }
    devServer: {    
        contentBase:'./dist',
        port:'3000',
        hot:true
    },
    resolve:{},
}

常用插件

  • html-webpack-plugin: 创建html并插入script标签
  • autoprefixer: 给cssh加前缀
  • mini-css-extract-plugin: 抽离css样式link到html
  • webpack-dev-server: 启动webpack服务
  • webpack-dev-middleware:webpack服务集成到本地的服务
  • uglifyjs-webpack-plugin:压缩js
  • optimize-css-assets-webpack-plugin:压缩css
  • clean-webpack-plugin:清空目录

结语

学习webpack的过程是从官方文档开始学习的,照着敲了一遍,然后搜索相关的内容。