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 都会参考到 就不写了。

  • 相关阅读:
    web api 设置允许跨域,并设置预检请求时间
    T4模板
    DDD模式
    Vue watch用法
    第三章--第五节:集合
    简单的Python API爬虫与数据分析教程--目录
    第三章--第四节:字典
    第三章--第三节(补充):列表排序
    汇总张小龙在知乎上的问答
    第三章--第三节:列表
  • 原文地址:https://www.cnblogs.com/wenruo/p/10022112.html
Copyright © 2011-2022 走看看