zoukankan      html  css  js  c++  java
  • Webpack学习笔记

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

    我的环境 Mac OS, node: v8.11.1, npm: 5.6.0, Webpack: 3.12.0

    0. 什么是Webpack

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

    1. 简单的应用

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

    package.json

    {
      "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 文件。

    // 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.html

    <!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>

    目录结构是这样的:

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

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

    首先安装 Webpack 。这里使用 Webpack3 版本3到4有很多变化,如果你用的4,基本就不用看下去了。

    安装命令: 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 都会参考到 就不写了。

  • 相关阅读:
    linux之awk命令
    HDU 2097 Sky数 进制转换
    HDU 2077 汉诺塔IV
    HDU 2094 产生冠军 dfs加map容器
    HDU 2073 叠框
    HDU 2083 简易版之最短距离
    HDU 2063 过山车 二分匹配
    天梯 1014 装箱问题
    天梯 1214 线段覆盖
    天梯 1098 均分纸牌
  • 原文地址:https://www.cnblogs.com/wenruo/p/10022112.html
Copyright © 2011-2022 走看看