webpack:从入门到真实项目配置

目前本身组建的一个团队正在写一份面试图谱,将会在七月中旬开源。内容十分丰富,初版会开源前端方面知识和程序员必备知识,后期会逐步写入后端方面知识。由于工程所涉及内容太多(目前已经写了一个半月),而且还需翻译成英文,因此所需时间较长。有兴趣的同窗能够 Follow 个人 Github 获得最快的更新消息。css

该文使用的 Webpack 版本为 3.6.0,本文分两部分。第一步是简单的使用 webpack,第二部分经过一个真实项目来配置 webpack,没有使用任何的 CLI,都是一步步配置直到完成生产代码的打包。这是本项目对应的仓库,每一个小节基本都对应了一次 commit。html

这是本文的大纲,若是以为有兴趣你就能够往下看了 前端

Webpack 究竟是什么

自从出现模块化之后,你们能够将本来一坨代码分离到个个模块中,可是由此引起了一个问题。每一个 JS 文件都须要从服务器去拿,由此会致使加载速度变慢。Webpack 最主要的目的就是为了解决这个问题,将全部小文件打包成一个或多个大文件,官网的图片很好的诠释了这个事情,除此以外,Webpack 也是一个能让你使用各类前端新技术的工具。node

简单使用

安装

在命令行中依次输入react

mkdir  webpack-demo
cd webpack-demo
// 建立 package.json,这里会问一些问题,直接回车跳过就行
npm init 
// 推荐这个安装方式,固然你也安装在全局环境下
// 这种安装方式会将 webpack 放入 devDependencies 依赖中
npm install --save-dev webpack
复制代码

而后按照下图建立文件 webpack

在如下文件写入代码git

// sum.js
// 这个模块化写法是 node 环境独有的,浏览器原生不支持使用
module.exports = function(a, b) {
    return a + b
}
// index.js
var sum = require('./sum')
console.log(sum(1, 2))
复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <title>Document</title>
</head>
<body>
    <div id="app"></div>
    <script src="./build/bundle.js"></script>
</body>
</html>
复制代码

如今咱们开始配置最简单的 webpack,首先建立 webpack.config.js 文件,而后写入以下代码程序员

// 自带的库
const path = require('path')
module.exports = {
    entry:  './app/index.js', // 入口文件
    output: {
      path: path.resolve(__dirname, 'build'), // 必须使用绝对地址,输出文件夹
      filename: "bundle.js" // 打包后输出文件的文件名
    }
  }
复制代码

如今咱们能够开始使用 webpack 了,在命令行中输入github

node_modules/.bin/webpack
复制代码

没问题的话你应该能够看到相似的样子 web

能够发现本来两个 JS 文件只有 100B,可是打包后却增加到 2.66KB,这之中 webpack 确定作了什么事情,咱们去 bundle.js 文件中看看。

把代码简化之后,核心思路是这样的

var array = [(function () {
        var sum = array[1]
        console.log(sum(1, 2))
    }),
    (function (a,b) {
        return a + b
    })
]
array[0]() // -> 3
复制代码

由于 module.export 浏览器是不支持的,因此 webpack 将代码改为浏览器能识别的样子。如今将 index.html 文件在浏览器中打开,应该也能够看到正确的 log。

咱们以前是在文件夹中安装的 webpack,每次要输入 node_modules/.bin/webpack 过于繁琐,能够在 package.json 以下修改

"scripts": {
    "start": "webpack"
  },
复制代码

而后再次执行 npm run start,能够发现和以前的效果是相同的。简单的使用到此为止,接下来咱们来探索 webpack 更多的功能。

Loader

Loader 是 webpack 一个很强大功能,这个功能可让你使用不少新的技术。

Babel

Babel 可让你使用 ES2015/16/17 写代码而不用顾忌浏览器的问题,Babel 能够帮你转换代码。首先安装必要的几个 Babel 库

npm i --save-dev babel-loader babel-core babel-preset-env
复制代码

先介绍下咱们安装的三个库

  • babel-loader 用于让 webpack 知道如何运行 babel
  • babel-core 能够看作编译器,这个库知道如何解析代码
  • babel-preset-env 这个库能够根据环境的不一样转换代码

接下来更改 webpack-config.js 中的代码

module.exports = {
// ......
    module: {
        rules: [
            {
            // js 文件才使用 babel
                test: /\.js$/,
             // 使用哪一个 loader
                use: 'babel-loader',
            // 不包括路径
                exclude: /node_modules/
            }
        ]
    }
}
复制代码

配置 Babel 有不少方式,这里推荐使用 .babelrc 文件管理。

// ..babelrc
{
    "presets": ["babel-preset-env"]
}
复制代码

如今将以前 JS 的代码改为 ES6 的写法

// sum.js
export default (a, b) => {
    return a + b
}
// index.js
import sum from './sum'
console.log(sum(1, 2))
复制代码

执行 npm run start,再观察 bundle.js 中的代码,能够发现代码被转换过了,而且一样能够正常 输出3。

固然 Babel 远不止这些功能,有兴趣的能够前往官网本身探索。

处理图片

这一小节咱们将使用 url-loaderfile-loader,这两个库不只能够处理图片,还有其余的功能,有兴趣的能够自行学习。

先安装库

npm i --save-dev url-loader file-loader
复制代码

建立一个 images 文件夹,放入两张图片,而且在 app 文件夹下建立一个 js 文件处理图片 ,目前的文件夹结构如图

// addImage.js
let smallImg = document.createElement('img')
// 必须 require 进来
smallImg.src = require('../images/small.jpeg')
document.body.appendChild(smallImg)

let bigImg = document.createElement('img')
bigImg.src = require('../images/big.jpeg')
document.body.appendChild(bigImg)
复制代码

接下来修改 webpack.config.js 代码

module.exports = {
// ...
    module: {
        rules: [
            // ...
            {
            // 图片格式正则
                test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
                use: [
                  {
                    loader: 'url-loader',
                    // 配置 url-loader 的可选项
                    options: {
                    // 限制 图片大小 10000B,小于限制会将图片转换为 base64格式
                      limit: 10000,
                    // 超出限制,建立的文件格式
                    // build/images/[图片名].[hash].[图片格式]
                      name: 'images/[name].[hash].[ext]'
                   }
                  }
                ]
            }
        ]
    }
  }
复制代码

运行 npm run start,打包成功以下图

能够发现大的图片被单独提取了出来,小的图片打包进了 bundle.js 中。

在浏览器中打开 HTML 文件,发现小图确实显示出来了,可是却没有看到大图,打开开发者工具栏,能够发现咱们大图的图片路径是有问题的,因此咱们又要修改 webpack.config.js 代码了。

module.exports = {
    entry:  './app/index.js', // 入口文件
    output: {
      path: path.resolve(__dirname, 'build'), // 必须使用绝对地址,输出文件夹
      filename: "bundle.js", // 打包后输出文件的文件名
      publicPath: 'build/' // 知道如何寻找资源
    }
    // ...
  }
复制代码

最后运行下 npm run start,编译成功了,再次刷新下页面,能够发现此次大图被正确的显示了。下一小节咱们将介绍如何处理 CSS 文件。

处理 CSS 文件

添加 styles 文件夹,新增 addImage.css 文件,而后在该文件中新增代码

img {
    border: 5px black solid;
}
.test {border: 5px black solid;}
复制代码

这一小节咱们先使用 css-loaderstyle-loader 库。前者可让 CSS 文件也支持 impost,而且会解析 CSS 文件,后者能够将解析出来的 CSS 经过标签的形式插入到 HTML 中,因此后面依赖前者。

npm i --save-dev css-loader style-loader
复制代码

首先修改 addImage.js 文件

import '../styles/addImage.css'

let smallImg = document.createElement('img')
smallImg.src = require('../images/small.jpeg')
document.body.appendChild(smallImg)

// let bigImg = document.createElement('img')
// bigImg.src = require('../images/big.jpeg')
// document.body.appendChild(bigImg)
复制代码

而后修改 webpack.config.js 代码

module.exports = {
// ...
    module: {
      rules: [
        {
            test: /\.css$/,
            use: ['style-loader',
                {
                    loader: 'css-loader',
                    options: {
                        modules: true
                       }
                }
            ]
        },
      ]
    }
  }
复制代码

运行下 npm run start,而后刷新页面,能够发现图片被正确的加上了边框,如今咱们来看一下 HTML 的文件结构

从上图能够看到,咱们在 addImage.css 文件中写的代码被加入到了 style 标签中,而且由于咱们开启了 CSS 模块化的选项,因此 .test 被转成了惟一的哈希值,这样就解决了 CSS 的变量名重复问题。

可是将 CSS 代码整合进 JS 文件也是有弊端的,大量的 CSS 代码会形成 JS 文件的大小变大,操做 DOM 也会形成性能上的问题,因此接下来咱们将使用 extract-text-webpack-plugin 插件将 CSS 文件打包为一个单独文件

首先安装 npm i --save-dev extract-text-webpack-plugin

而后修改 webpack.config.js 代码

const ExtractTextPlugin = require("extract-text-webpack-plugin")

module.exports = {
// ....
    module: {
      rules: [
        {
          test: /\.css$/,
          // 写法和以前基本一致
          loader: ExtractTextPlugin.extract({
          // 必须这样写,不然会报错
                fallback: 'style-loader',
                use: [{
                    loader: 'css-loader',
                    options: { 
                        modules: true
                    }
                }]
            })
        ]
        }
      ]
    },
    // 插件列表
    plugins: [
    // 输出的文件路径
      new ExtractTextPlugin("css/[name].[hash].css")
    ]
  }
复制代码

运行下 npm run start,能够发现 CSS 文件被单独打包出来了

可是这时候刷新页面会发现图片的边框消失了,那是由于咱们的 HTML 文件没有引用新的 CSS 文件,因此这里须要咱们手动引入下,在下面的章节咱们会经过插件的方式自动引入新的文件。

接下来,会用一个项目来继续咱们的 webpack 学习,在这以前,先 clone 一下项目。该项目原地址是 这里,由于使用的 webpack 版本过低,而且依赖的库也有点问题,故我将项目拷贝了过来并修改了几个库的版本号。

请依次按照如下代码操做

git clone https://github.com/KieSun/webpack-demo.git
cd webpack-demo
// 切换到 0.1 标签上并建立一个新分支
git checkout -b demo 0.1
cd project
npm i 
// 查看分支是否为 demo,没问题的话就能够进行下一步
复制代码

如何在项目中使用 webpack

项目中已经配置了很简单的 babel 和 webpack,直接运行 npm run build 便可

这时候你会发现这个 bundle.js 竟然有这么大,这确定是不能接受的,因此接下来章节的主要目的就是将单个文件拆分为多个文件,优化项目。

分离代码

先让咱们考虑下缓存机制。对于代码中依赖的库不多会去主动升级版本,可是咱们本身的代码却每时每刻都在变动,因此咱们能够考虑将依赖的库和本身的代码分割开来,这样用户在下一次使用应用时就能够尽可能避免重复下载没有变动的代码,那么既然要将依赖代码提取出来,咱们须要变动下入口和出口的部分代码。

// 这是 packet.json 中 dependencies 下的
const VENOR = ["faker",
  "lodash",
  "react",
  "react-dom",
  "react-input-range",
  "react-redux",
  "redux",
  "redux-form",
  "redux-thunk"
]

module.exports = {
// 以前咱们都是使用了单文件入口
// entry 同时也支持多文件入口,如今咱们有两个入口
// 一个是咱们本身的代码,一个是依赖库的代码
  entry: {
  // bundle 和 vendor 都是本身随便取名的,会映射到 [name] 中
    bundle: './src/index.js',
    vendor: VENOR
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].js'
  },
  // ...
 }
复制代码

如今咱们 build 一下,看看是否有惊喜出现

真的有惊喜。。为何 bundle 文件大小压根没变。这是由于 bundle 中也引入了依赖库的代码,刚才的步骤并无抽取 bundle 中引入的代码,接下来让咱们学习如何将共同的代码抽取出来。

抽取共同代码

在这小节咱们使用 webpack 自带的插件 CommonsChunkPlugin

module.exports = {
//...
  output: {
    path: path.join(__dirname, 'dist'),
    // 既然咱们但愿缓存生效,就应该每次在更改代码之后修改文件名
    // [chunkhash]会自动根据文件是否更改而更换哈希
    filename: '[name].[chunkhash].js'
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
    // vendor 的意义和以前相同
    // manifest文件是将每次打包都会更改的东西单独提取出来,保证没有更改的代码无需从新打包,这样能够加快打包速度
      names: ['vendor', 'manifest'],
      // 配合 manifest 文件使用
      minChunks: Infinity
    })
  ]
};
复制代码

当咱们从新 build 之后,会发现 bundle 文件很明显的减少了体积

可是咱们使用哈希来保证缓存的同时会发现每次 build 都会生成不同的文件,这时候咱们引入另外一个插件来帮助咱们删除不须要的文件。

npm install --save-dev clean-webpack-plugin
复制代码

而后修改配置文件

module.exports = {
//...
  plugins: [
  // 只删除 dist 文件夹下的 bundle 和 manifest 文件
    new CleanWebpackPlugin(['dist/bundle.*.js','dist/manifest.*.js'], {
    // 打印 log
      verbose: true,
      // 删除文件
      dry: false
    }),
  ]
};
复制代码

而后 build 的时候会发现以上文件被删除了。

由于咱们如今将文件已经打包成三个 JS 了,之后也许会更多,每次新增 JS 文件咱们都须要手动在 HTML 中新增标签,如今咱们能够经过一个插件来自动完成这个功能。

npm install html-webpack-plugin --save-dev
复制代码

而后修改配置文件

module.exports = {
//...
  plugins: [
  // 咱们这里将以前的 HTML 文件当作模板
  // 注意在以前 HTML 文件中请务必删除以前引入的 JS 文件
    new HtmlWebpackPlugin({
      template: 'index.html'
    })
  ]
};
复制代码

执行 build 操做会发现同时生成了 HTML 文件,而且已经自动引入了 JS 文件

按需加载代码

在这一小节咱们将学习如何按需加载代码,在这以前的 vendor 入口我发现忘记加入 router 这个库了,你们能够加入这个库而且从新 build 下,会发现 bundle 只有不到 300KB 了。

如今咱们的 bundle 文件包含了咱们所有的本身代码。可是当用户访问咱们的首页时,其实咱们根本无需让用户加载除了首页之外的代码,这个优化咱们能够经过路由的异步加载来完成。

如今修改 src/router.js

// 注意在最新版的 V4路由版本中,更改了按需加载的方式,若是安装了 V4版,能够自行前往官网学习
import React from 'react';
import { Router, Route, IndexRoute, hashHistory } from 'react-router';

import Home from './components/Home';
import ArtistMain from './components/artists/ArtistMain';

const rootRoute = {
  component: Home,
  path: '/',
  indexRoute: { component: ArtistMain },
  childRoutes: [
    {
      path: 'artists/new',
      getComponent(location, cb) {
        System.import('./components/artists/ArtistCreate')
          .then(module => cb(null, module.default))
      }
    },
    {
      path: 'artists/:id/edit',
      getComponent(location, cb) {
        System.import('./components/artists/ArtistEdit')
          .then(module => cb(null, module.default))
      }
    },
    {
      path: 'artists/:id',
      getComponent(location, cb) {
        System.import('./components/artists/ArtistDetail')
          .then(module => cb(null, module.default))
      }
    }
  ]
}

const Routes = () => {
  return (
    <Router history={hashHistory} routes={rootRoute} /> ); }; export default Routes; 复制代码

而后执行 build 命令,能够发现咱们的 bundle 文件又瘦身了,而且新增了几个文件

将 HTML 文件在浏览器中打开,当点击路由跳转时,能够在开发者工具中的 Network 一栏中看到加载了一个 JS 文件。

首页

点击右上角 Random Artist 之后

自动刷新

每次更新代码都须要执行依次 build,而且还要等上一会很麻烦,这一小节介绍如何使用自动刷新的功能。

首先安装插件

npm i --save-dev webpack-dev-server
复制代码

而后修改 packet.json 文件

"scripts": {
    "build": "webpack",
    "dev": "webpack-dev-server --open"
  },
复制代码

如今直接执行 npm run dev 能够发现浏览器自动打开了一个空的页面,而且在命令行中也多了新的输出

等待编译完成之后,修改 JS 或者 CSS 文件,能够发现 webpack 自动帮咱们完成了编译,而且只更新了须要更新的代码

可是每次从新刷新页面对于 debug 来讲很不友好,这时候就须要用到模块热替换了。可是由于项目中使用了 React,而且 Vue 或者其余框架都有本身的一套 hot-loader,因此这里就略过了,有兴趣的能够本身学习下。

生成生产环境代码

如今咱们能够将以前所学和一些新加的插件整合在一块儿,build 生产环境代码。

npm i --save-dev url-loader optimize-css-assets-webpack-plugin file-loader extract-text-webpack-plugin
复制代码

修改 webpack 配置

var webpack = require('webpack');
var path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin')
var CleanWebpackPlugin = require('clean-webpack-plugin')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')

const VENOR = ["faker",
  "lodash",
  "react",
  "react-dom",
  "react-input-range",
  "react-redux",
  "redux",
  "redux-form",
  "redux-thunk",
  "react-router"
]

module.exports = {
  entry: {
    bundle: './src/index.js',
    vendor: VENOR
  },
  // 若是想修改 webpack-dev-server 配置,在这个对象里面修改
  devServer: {
    port: 8081
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].[chunkhash].js'
  },
  module: {
    rules: [{
        test: /\.js$/,
        use: 'babel-loader'
      },
      {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        use: [{
            loader: 'url-loader',
            options: {
                limit: 10000,
                name: 'images/[name].[hash:7].[ext]'
            }
        }]
    },
    {
        test: /\.css$/,
        loader: ExtractTextPlugin.extract({
            fallback: 'style-loader',
            use: [{
            // 这边其实还可使用 postcss 先处理下 CSS 代码
                loader: 'css-loader'
            }]
        })
    },
    ]
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: ['vendor', 'manifest'],
      minChunks: Infinity
    }),
    new CleanWebpackPlugin(['dist/*.js'], {
      verbose: true,
      dry: false
    }),
    new HtmlWebpackPlugin({
      template: 'index.html'
    }),
    // 生成全局变量
    new webpack.DefinePlugin({
      "process.env.NODE_ENV": JSON.stringify("process.env.NODE_ENV")
    }),
    // 分离 CSS 代码
    new ExtractTextPlugin("css/[name].[contenthash].css"),
    // 压缩提取出的 CSS,并解决ExtractTextPlugin分离出的 JS 重复问题
    new OptimizeCSSPlugin({
      cssProcessorOptions: {
        safe: true
      }
    }),
    // 压缩 JS 代码
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false
      }
    })
  ]
};

复制代码

修改 packet.json 文件

"scripts": {
    "build": "NODE_ENV=production webpack -p",
    "dev": "webpack-dev-server --open"
  }
复制代码

执行 npm run build

能够看到咱们在经历了这么多步之后,将 bundle 缩小到了只有 27.1KB,像 vendor 这种经常使用的库咱们通常可使用 CDN 的方式外链进来。

补充

webpack 配置上有些实用的小点在上文没有提到,统一在这里提一下。

module.exports = {
  resolve: {
  // 文件扩展名,写明之后就不须要每一个文件写后缀
    extensions: ['.js', '.css', '.json'],
 // 路径别名,好比这里可使用 css 指向 static/css 路径
    alias: {
      '@': resolve('src'),
      'css': resolve('static/css')
    }
  },
  // 生成 source-map,用于打断点,这里有好几个选项
  devtool: '#cheap-module-eval-source-map',
}
复制代码

后记

若是你是跟着本文一个个步骤敲下来的,那么大部分的 webpack 配置你应该都是能够看懂了,而且本身应该也知道如何去配置。谢谢你们看到这里,这是本项目对应的仓库,每一个小节基本都对应了一次 commit。

文章较长,有错误也不免,若是你发现了任何问题或者我有任何表述的不明白的地方,均可以留言给我。

相关文章
相关标签/搜索