Webpack学习笔记

写过 React ,用的是 create-react-app ,写过 Vue ,用的是 vue-cli , 第一次想了解一下 Webpack 。javascript

个人环境 Mac OS, node: v8.11.1, npm: 5.6.0, Webpack: 3.12.0css

 

0. 什么是Webpack

我就不说乱七八糟的术语了,就是把不少的 JS 文件打包到一个文件(固然也可能不止一个)的工具,方便咱们写模块化的 JS 代码。而经过一些 plugin 和 loader 可能提供一些其余有用的功能以及处理其余格式的文件。html

1. 简单的应用

先建立一个文件夹,在终端运行命令  npm init 来建立一个  package.json 文件,这个文件用来描述项目信息,随便填或者一直回车就能够。前端

package.jsonvue

{
  "name": "webpack-study-note-1",
  "version": "1.0.0",
  "description": "webpack学习笔记",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": "https://github.com/G-lory/front-end-practice/tree/master/webpack/study-notes",
  "author": "G-lory",
  "license": "ISC"
}

 

先建一个 src 文件夹用来 js 源文件。建立两个 js 文件。java

// index.js
const foo = require('./others.js');

let app = document.getElementById('app');
for (let i = 0; i < 10; i++) {
    let p = document.createElement('p');
    p.innerText = foo(i);
    app.appendChild(p);
}

// others.js
function foo(idx) {
    return `the ${idx + 1}th row`;
}

module.exports = foo;

并在根目录建立 index.htmlnode

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>webpack study notes</title>
</head>
<body>
    <div id="app"></div>
</body>
</html>

目录结构是这样的:react

├── src
│ ├── index.js
│ └── others.js
├── index.html
└── package.json

若是想在 index.html 引用全部的js文件,就要经过<srcipt>标签将js文件所有导入,并且还要注意顺序。如今经过 module.exports 和 require 在JS中引用,而后把这些文件打包成一个文件,那么 index.html 直接引用最终的那个 js 文件就能够了。webpack

首先安装 Webpack 。这里使用 Webpack3 版本3到4有不少变化,若是你用的4,基本就不用看下去了。git

安装命令: npm install webpack@3 --save-dev 

其中 install 可简写为 i , --save-dev 可简写为 -D,表示仅在开发环境依赖,会在package.json的 devDependencies 字段记录 。相对的是 --save 表示运行时依赖,简写为 -S, 会在package.json的 dependencies 字段记录。@3 表示指定安装版本。

项目下会生成一个 node_modules 文件夹。里面是安装的依赖包。不用去管这个文件夹。

而后在根目录下建立 webpack.config.js 文件。这是webpack默认的配置文件名。这个文件其实就是一个普通的 js 脚本文件,能够经过require引用一些模块,最后导出配置对象。

// webpack.config.js
var path = require('path'); // node 内置模块

module.exports = {
    entry: './src/index.js', // 入口文件 至关于 entry: { main: './src/index.js' }

    output: { // 出口
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
    }
}

一个最简单的配置文件,指定了输入输出。输入和输出均可以指定多个,这里暂时用不到。

在 package.json 中添加打包命令

"scripts": {
    "build": "webpack"
},

而后在命令行执行  npm run build 就能够进行打包了,会生成文件 /dist/bundle.js 。

打开文件能够看到,前面是 webpack 生成的一些代码,后面就是 index.js 和 others.js 中的代码。

而后在 index.html 中引用文件

<script src="dist/bundle.js"></script>

在浏览器打开 index.html 文件 正常运行。

完整代码见: https://github.com/G-lory/front-end-practice/tree/master/webpack/study-notes/step1

2. 使用 loader 和 plugin

个人我的感受,loader就是处理文件的,先使用loader将文件转换成想要的样子,好比Webpack默认不能处理的图片要先使用file-loader处理,es6先使用babel-loder处理成es5防止浏览器不兼容等等。

而 plugin 能够作一些其余的神奇并且颇有用的事情(我在说什么……

以前的代码使用的ES6,如今就尝试下把它转换成ES5,须要使用 babel-loader。

安装:

npm install babel-loader@7 babel-core babel-preset-env -D

注意这里为了和webapck3兼容,须要指定 babel-loader 版本。

而后修改 webpack 配置文件。这里 babel 的配置含义可见 https://segmentfault.com/a/1190000008159877

var path = require('path');

module.exports = {
    entry: './src/index.js', // 入口

    output: { // 出口
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
    },

    module: { // 配置loader
        rules: [
            {
                test: /\.jsx?/,             // 正则表达式 匹配文件名
                exclude: /node_modules/,    // exclude 表示排除的路径 也能够添加 include 字段设置匹配路径
                use: {
                    loader: 'babel-loader', // 对符合上面约束条件的文件 使用的 loader
                    options: {
                        presets: ['env']
                    }
                }
            }
        ]
    }
}

babel 默认是不进行转换的,须要设置插件,这里经过 presets 设置插件指定代码的转换规则。

再次执行 npm run build 能够看到 bundle.js 中 let 都变成了 var。说明 babel 生效了。

接下来在再试一下 Promise。

修改 JS 代码

//index.js
const foo = require('./others.js');

let app = document.getElementById('app');
for (let i = 0; i < 10; i++) {
    let p = document.createElement('p');
    foo(i).then(content => {
        p.innerText = content;
        app.appendChild(p);
    })
}
// others.js
function foo(idx) {
    return new Promise(function (resolve, reject) {
        resolve(`the ${idx + 1}th row`);
    })
}

module.exports = foo;

打包后发现 Promise 相关带并无进行处理。原来上面的配置只能转换ES的新语法,对于新的API(Promise、Set、Map 等新增对象,Object.assign、Object.entries等静态方法。)却没有做用。

有两种方式解决这个问题,babel-polyfill 或 babel-runtime,前者默认所有加载,后者是按需加载。这么说好像有错....能够阅读 https://juejin.im/post/5a96859a6fb9a063523e2591

安装:

npm install babel-plugin-transform-runtime -D

而后修改 babel 配置

{
    test: /\.js$/,              // 正则表达式 匹配文件名
    exclude: /node_modules/,    // exclude 表示排除的路径 也能够添加 include 字段设置匹配路径
    use: {
        loader: 'babel-loader', // 对符合上面约束条件的文件 使用的 loader
        options: {
            presets: ['env'],
            plugins: ['transform-runtime']
        }
    }
}

如今再打包试一下会发现 bundle.js 文件的体积大了一些 那是多了 Promise 的 polyfill,打开 bundle.js 能看到相关代码。

 

上面是 loader 的使用,再试一下 plugin 的使用。

html-webpack-plugin 能够生成一个 html 文件,把生成的 js 文件自动注入其中。

安装

npm install html-webpack-plugin -D

在配置文件中添加 plugins 字段

var path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    // ...
    plugins: [
        new HtmlWebpackPlugin({
            filename: 'index.html', // 生成文件名
            template: 'index.html'  // 模板
        })
    ]
}

如今能够把 /index.js 中引入 js 的语句删除了,而后从新打包。

能够看到dist文件夹生成了一个 index.html 文件,该文件中引入了 js。

完整代码见: https://github.com/G-lory/front-end-practice/tree/master/webpack/study-notes/step2

3. 使用 Webpack 打包 React

react 使用的是 jsx 语法,须要用 babel 将 jsx 转换成 js。

首先安装 React

npm install react react-dom --save

而后安装 babel 转换 react 文件的插件

npm i babel-preset-react -D

在 src 文件夹的文件改成下面几个文件:

index.jsx

// index.jsx
import React from 'react';
import { render } from 'react-dom';
import Input from './input';
import List from './list';

class App extends React.Component {

    constructor(props) {
        super(props);
        this.state = {
            list: []
        };
    }

    addItem(item) {
        this.setState({
            list: this.state.list.concat(item)
        })
    }

    removeItem(idx) {
        this.setState({
            list: this.state.list.filter((it, id) => id !== idx)
        })
    }


    render() {
        return(
          <div className='todoList'>
              <Input handleSubmit={this.addItem.bind(this)} />
              <List list={this.state.list} handleRemove={this.removeItem.bind(this)} />
          </div>
        )
    }
}

render(<App />, document.getElementById('app'));

input.jsx

// input.jsx
import React, { Component } from 'react';

class Input extends Component {
    constructor(props) {
        super(props);
        this.state = {
            content: ''
        };
    }
    submit() {
        if (this.state.content === '') return ;
        // 提交数据并清空
        this.props.handleSubmit(this.state.content);
        this.setState({
            content: ''
        })
    }
    handleChange(e) {
        this.setState({
            content: e.target.value
        })
    }
    render() {
        return (
          <div className='input'>
              <p>
                    <textarea
                      value={this.state.content}
                      onChange={this.handleChange.bind(this)}

                    >
                    </textarea>
              </p>
              <p className='btn'>
                  <button onClick={this.submit.bind(this)}>提交</button>
              </p>
          </div>
        )
    }
}

export default Input;

list.jsx

// list.jsx
import React, { Component } from 'react';

class List extends Component {
    render() {
        return (
          <div>
              {
                  this.props.list.map((item, idx) =>
                    <div className='listItem' key={idx}>
                        <span>{item}</span>
                        <button onClick={() => this.props.handleRemove(idx)}>删除</button>
                    </div>
                  )
              }
          </div>
        )
    }
}

export default List;

而后修改 webpack.config.js

var path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: './src/index.jsx', // 入口

    output: { // 出口
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
    },

    module: { // 配置loader
        rules: [
            {
                test: /\.jsx?/,             // 正则表达式 匹配文件名
                exclude: /node_modules/,    // exclude 表示排除的路径 也能够添加 include 字段设置匹配路径
                use: {
                    loader: 'babel-loader', // 对符合上面约束条件的文件 使用的 loader
                    options: {
                        presets: ['env', 'react'],
                        plugins: ['transform-runtime']
                    }
                }
            }
        ]
    },

    resolve: { // 代码模块路径解析的配置
        extensions: ['.js', '.jsx'] // 进行模块路径解析时,webpack 会尝试补全后缀名来进行查找
    },

    plugins: [
        new HtmlWebpackPlugin({
            filename: 'index.html', // 生成文件名
            template: 'index.html'  // 模板
        })
    ]
}

此次添加了字段 resolve.extensions 注意到 index.jsx 引用文件时没有添加文件后缀,由于经过 resolve.extensions 的配置 Webpack 会尝试补全指定后缀来查找。尝试补全的顺序是数组中元素的顺序。

打包后打开 dist/index.html 文件,能够看到一个虽然很丑可是能正常运行的页面。

打开 dist/bundle.js 能够发现文件的长度达到了 2w+ 行。那是由于咱们把 react 也打包进来了。

react 是咱们直接引入的代码,里面的内容不多会更改,而咱们本身写页面会常常变化,因此为了充分利用页面缓存,但愿把 node_modules 中的代码单独打包成一个 js 文件。

修改 webpack.config.js

var path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var webpack = require('webpack');

module.exports = {
    ...

    plugins: [
        new HtmlWebpackPlugin({
            filename: 'index.html', // 生成文件名
            template: 'index.html'  // 模板
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor', // 使用 vendor 入口做为公共部分
            filename: "vendor.js",
            minChunks: (module, count) => {
                return module.context && module.context.includes("node_modules");
            }
        })
    ]
}

如今再次打包会出现 dist 下面会出现三个文件 而 bundle.js 中只有几百行代码了。

咱们也能够给文件名添加 hash 防止浏览器缓存。

var path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var webpack = require('webpack');

module.exports = {
    ...

    output: { // 出口
        path: path.resolve(__dirname, 'dist'),
        filename: 'js/[name].[chunkhash].js',
    },

    ...

    plugins: [
        new HtmlWebpackPlugin({
            filename: 'index.html', // 生成文件名
            template: 'index.html'  // 模板
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor', // 使用 vendor 入口做为公共部分
            filename: "js/[name].[chunkhash].js",
            minChunks: (module, count) => {
                return module.context && module.context.includes("node_modules");
            }
        })
    ]
}

完整代码见: https://github.com/G-lory/front-end-practice/tree/master/webpack/study-notes/step3

4. 使用 webpack-dev-server 搭建本地环境

由于打包会花费很长时间 尤为是文件多的时候。咱们开发时须要获取及时反馈,而不能每次打包后观察错对。

使用 webpack-dev-server 能够很简单的启动一个本地静态服务。

安装

npm i webpack-dev-server@2 -D

为了配合 Webpack3 须要指定版本。

而后在 package.json 添加脚本命令 start 

"scripts": {
    "start": "webpack-dev-server",
    "build": "webpack"
},

而后运行  npm run start  会默认在 http://localhost:8080/ 启动一个服务器 打开以后和以前打包的页面是同样的 尝试修改文件 会发现页面会实时变化。

能够在配置文件添加 devServer 字段配置 webpack-dev-server 的选项,好比下面配置打开地址和端口号

devServer: {
    host: 'localhost',
    port: 8888,
    open: true // 自动打开浏览器
}

完整代码见: https://github.com/G-lory/front-end-practice/tree/master/webpack/study-notes/step4

5. 使用 css 和 less

上面完成的页面很丑,由于尚未加入样式。没用过less,可是sass-node那个包实在是很麻烦,仍是选择了less,毕竟只是个demo。

不论是less仍是css,Webpack都不认识,须要加入loader来处理。

安装

npm i less less-loader css-loader@0 style-loader -D

而后按照官网的提示,在配置文件的 rules 添加代码

module: { // 配置loader
    rules: [
        {
            test: /\.jsx?/,             // 正则表达式 匹配文件名
            exclude: /node_modules/,    // exclude 表示排除的路径 也能够添加 include 字段设置匹配路径
            use: {
                loader: 'babel-loader', // 对符合上面约束条件的文件 使用的 loader
                options: {
                    presets: ['env', 'react'],
                    plugins: ['transform-runtime']
                }
            }
        },
        {
            test: /\.less$/,
            include: [
                path.resolve(__dirname, 'src')
            ],
            use: [{
                loader: 'style-loader' // creates style nodes from JS strings
            }, {
                loader: 'css-loader' // translates CSS into CommonJS
            }, {
                loader: 'less-loader' // compiles Less to CSS
            }]
        }
    ]
},

而后添加样式文件

/* index.less */
* {
  margin: 0;
  padding: 0;
}

.todoList {
  padding: 10px 50px;
}

/* input.less */
.input {
  margin-bottom: 10px;
  textarea {
    width: calc(100% - 10px);
    height: 50px;
    color: #9c9c9c;
    border-radius: 5px;
    resize: none;
    outline: none;
    padding: 5px;
    margin-bottom: 5px;
  }
  .btn {
    text-align: right;
    button {
      border: none;
      outline: none;
      background-color: transparent;
    }
  }
}

/* list.less */
.listItem {
  height: 40px;
  line-height: 40px;
  border: 1px solid #d6d6d6;
  display: flex;
  margin-bottom: 10px;
  padding: 10px;

  span {
    flex-grow: 1;
    color: #9c9c9c;
  }

  button {
    border: none;
    outline: none;
    background-color: transparent;
  }
}

而后再每一个文件分别引用就能够。

import './index.less'; /* index.jsx */
import './input.less'; /* input.jsx */
import './list.less';  /* list.jsx */

查看页面,样式已经生效,可是这样的问题是,全部的样式都是全局样式,容易发生命名冲突的状况,css模块化能够解决这个问题。

css-loader 有一个 modules 可配置项,表示是否模块化,配置改成:

{
    test: /\.less$/,
    include: [
        path.resolve(__dirname, 'src')
    ],
    use: [{
        loader: 'style-loader' // creates style nodes from JS strings
    }, {
        loader: 'css-loader', // translates CSS into CommonJS
        options: {
            modules: true,
            localIdentName: '[name]__[local]___[hash:base64:5]' // 生成的css类名 -> [文件名]__[类名]___[哈希]
        }
    }, {
        loader: 'less-loader' // compiles Less to CSS
    }]
}

如今可使用模块化样式了。

如今引用类是须要酱紫

import styles from './index.less';
....
render() {
    return(
      <div className={styles.todoList}>
          <Input handleSubmit={this.addItem.bind(this)} />
          <List list={this.state.list} handleRemove={this.removeItem.bind(this)} />
      </div>
    )
}

而全局样式须要酱紫:

:global(*) {
  margin: 0;
  padding: 0;
}

 

以前处于缓存的考虑,把 node_modules 单独打包,如今出于一样的考虑,须要把 css 也单独打包。

以前的 loader 是把 css 转成了 js 代码,而把 css 单独打包成一个文件,须要使用 ExtractTextPlugin。

安装:

npm install extract-text-webpack-plugin -D

修改配置文件

var path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var webpack = require('webpack');
var ExtractTextPlugin = require("extract-text-webpack-plugin");

module.exports = {
    // ...
    module: { // 配置loader
        rules: [
            // ...
            {
                test: /\.less$/,
                include: [
                    path.resolve(__dirname, 'src')
                ],
                use: ExtractTextPlugin.extract({
                    fallback: 'style-loader',
                    use: [{
                        loader: 'css-loader', // translates CSS into CommonJS
                        options: {
                            modules: true,
                            localIdentName: '[name]__[local]___[hash:base64:5]' // 生成的css类名 -> [文件名]__[类名]___[哈希]
                        }
                    }, {
                        loader: 'less-loader' // compiles Less to CSS
                    }]
                })
            },
        ]
    },

    // ...

    plugins: [
        new HtmlWebpackPlugin({
            filename: 'index.html', // 生成文件名
            template: 'index.html'  // 模板
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor', // 使用 vendor 入口做为公共部分
            filename: "js/[name].[chunkhash].js",
            minChunks: (module, count) => {
                return module.context && module.context.includes("node_modules");
            }
        }),
        new ExtractTextPlugin('css/[name].[contenthash].css')
    ]
}

再次打包,如今CSS文件也单独分离出来了。

完整代码见: https://github.com/G-lory/front-end-practice/tree/master/webpack/study-notes/step5

6. 图片和雪碧图

每一个正经的前端应该都知道雪碧图是什么吧,反正我不知道……我还觉得是瀑布流什么的神奇效果……

经过 Webpack 的插件,能够自动把引入的图片生成雪碧图。也能够用 url-loader 来处理图片,这里没有选择使用。

首先 Webpack 不识别图片类型的文件 要引入 file-loader ,同时引入 webpack-spritesmith 用来生成雪碧图。

安装

npm install file-loader webpack-spritesmith -D

我找了两个图片

delete.png 和  submit.png 放到 /images 文件夹下面

处理图片要添加 loader

{
    test: /\.(png|jpg|gif)$/,
    use: [
        {
            loader: 'file-loader',
            options: {}
        }
    ]
}

生成雪碧图添加 plugins

var SpritesmithPlugin = require('webpack-spritesmith');

new SpritesmithPlugin({
    src: {
        cwd: path.resolve(__dirname, 'images'), // 多个图片所在的目录
        glob: '*.png' // 匹配图片的路径
    },
    target: {
        // 生成最终图片的路径
        image: path.resolve(__dirname, 'src/spritesmith-generated/sprite.png'),
        // 生成所需 less 代码
        css: path.resolve(__dirname, 'src/spritesmith-generated/sprite.less'),
    },
    apiOptions: {
        cssImageRef: "~sprite.png"
    }
})

如今打包的时候会在 /src/spritesmith-generated 生成雪碧图和所需的 less 代码

生成的雪碧图

尝试使用,修改 input.less 和 list.less

input.less

/* input.less */
@import './spritesmith-generated/sprite.less';

.input {
   /* ignore.. */
  .btn {
    text-align: right;
    button {
      .sprite(@submit);
      border: none;
      outline: none;
      background-color: transparent;
    }
  }
}

list.less

@import './spritesmith-generated/sprite.less';

.listItem {
  /* ignore.. */

  button {
    .sprite(@delete);
    border: none;
    outline: none;
    background-color: transparent;
  }
}

而后启动项目会发现报错了……

虽然也没看明白什么意思吧……反正就是在 less-loader 添加配置项  javascriptEnabled: true 

而后打包发现路径不对查了下发现须要给 ExtractTextPlugin 配置 publicPath

最后该规则改成

{
    test: /\.less$/,
    include: [
        path.resolve(__dirname, 'src'),
    ],
    use: ExtractTextPlugin.extract({
        fallback: 'style-loader',
        use: [{
            loader: 'css-loader', // translates CSS into CommonJS
            options: {
                modules: true,
                localIdentName: '[name]__[local]___[hash:base64:5]' // 生成的css类名 -> [文件名]__[类名]___[哈希]
            }
        }, {
            loader: 'less-loader', // compiles Less to CSS
            options: {
                javascriptEnabled: true
            }
        }],
        publicPath: "../"
    })
},

可是仍是有报错

这个我是真的不知道怎么解决,只是发现去掉 css module 就没有这个问题了,因而我删掉了 css module 部分……(配合标题 我TM是真的菜

而后就能够正常打包了。一个 To Do List 就勉强作好了……

完整代码见: https://github.com/G-lory/front-end-practice/tree/master/webpack/study-notes/step6

7. 总结

我仍是老老实实的用  create-react-app 和 vue-cli 吧……

8. 参考资料

每个用到的 loader 和 plugin 的 GitHub 都会参考到 就不写了。