lxtmx

lxtmx 查看完整档案

填写现居城市苏州大学  |  对外汉语 编辑  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

lxtmx 发布了文章 · 2019-06-11

webpack4.x配置指南

简介

鉴于webpack更新太快,总结下基础配置方法,理解有限,仅做抛砖引玉之用。

起步

初始化配置文件 package.json并安装webpack

mkdir webpack-demo && cd webpack-demo
npm init -y    //-y 初始化选项默认为 yes      
npm i webpack webpack-cli -D // -D 即 -save-dev    版本4.x以上需要安装webpack-cli

创建以下目录结构、文件和内容:

webpack-demo
  |- package.json
+ |- /src
+   |- index.js
//index.js
document.write("Hello webpack4!");

创建webpack配置文件

编写开发环境和生产环境彼此独立的webpack配置文件
先添加三个文件

 webpack-demo
  |- package.json
+ |- webpack.common.js
+ |- webpack.dev.js
+ |- webpack.prod.js
  |- /src
    |- index.js
  |- /node_modules

1.webpack.common.js

用到两个基本的插件

npm i clean-webpack-plugin html-webpack-plugin -D

clean-webpack-plugin:打包时自动清除输出文件夹中未用到的文件;
html-webpack-plugin:打包时会自动生成index.html并替换已有的index.html,bundle.js也会自行添加到 html 中。

  //webpack.common.js
    const path = require('path');
    const { CleanWebpackPlugin } = require('clean-webpack-plugin');  
    const HtmlWebpackPlugin = require('html-webpack-plugin');    
    
    module.exports = {
      entry: './src/index.js',
      plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
          title: 'index'
        })
      ],
      output: {
        filename: 'bundle.js',           
        path: path.resolve(__dirname, 'dist')    //定义输出文件夹dist路径
      }
    };

2.webpack.dev.js

先安装webpack-merge,用以合并通用配置文件与开发环境配置文件

npm i webpack-merge -D

安装开发服务器devServer,作用是修改代码后实时重新加载(刷新浏览器)

npm i webpack-dev-server -D
//webpack.dev.js
  const merge = require('webpack-merge');
  const common = require('./webpack.common.js');
  const webpack = require('webpack');
    
  module.exports = merge(common,{
     devServer: {                //启用开发服务器
       contentBase: './dist',   //告诉服务器从哪提供内容,只有在想要提供静态文件时才需要
       compress: true,          //一切服务都启用gzip 压缩
       host: '0.0.0.0',         //指定使用一个host,可用ip地址访问,没有的话如果别人访问会被禁止。默认localhost。
       port: '9999',            //指定端口号,如省略,默认为”8080“
       hot: true,               //启用模块热替换特性
       inline: true,            //启用内联模式,一段处理实时重载的脚本被插入到bundle中,并且构建消息会出现在浏览器控制台
       historyApiFallback: true,//开发单页应用时有用,依赖于HTML5 history API,设为true时所有跳转将指向index.html
     },
     plugins: [
         new webpack.HotModuleReplacementPlugin(),  //webpack内置的热更新插件
     ],
     mode: 'development'
  });

devServer的更多可选参数-https://www.webpackjs.com/con...

HotModuleReplacementPlugin 模块热替换(Hot Module Replacement)插件,用以在运行时更新发生改变的模块,从而无需进行完全刷新。

3.webpack.prod.js

同样用'webpack-merge'合并通用配置文件与生产环境配置文件

//webpack.prod.js
  const merge = require('webpack-merge');
  const common = require('./webpack.common.js');
    
  module.exports = merge(common,{
    mode: "production"
  });
关于mode

此时你可能会注意到配置文件中有个mode项,webpack4中新加,作用如下:

  • --mode production 生产环境

    不需要像旧版本一样定义node环境变量
    new webpack.DefinePlugin({"process.env.NODE_ENV":JSON.stringify("production") })

    ps:许多 library 将通过与 process.env.NODE_ENV 环境变量关联,以决定 library 中应该引用哪些内容。当使用 process.env.NODE_ENV === 'production' 时,一些 library 可能针对具体用户的环境进行代码优化,从而删除或添加一些重要代码。
    

    自动开启一些插件,如:
    uglifyjs-webpack-plugin js代码压缩(所以无需再单独使用)
    NoEmitOnErrorsPlugin 编译出错时跳过输出,以确保输出资源不包含错误
    ModuleConcatenationPlugin webpack3 添加的作用域提升(Scope Hoisting)

  • --mode development 开发环境

    自行定义node环境变量为development
    new webpack.DefinePlugin({"process.env.NODE_ENV":JSON.stringify("development") })
    使用 eval 构建 module, 提升增量构建速度
    自动开启一些插件,如
    NamedModulesPlugin 使用模块热替换(HMR)时会显示模块的相对路径

具体描述:

OptionDescription
developmentSets process.env.NODE_ENV to value development. Enables NamedChunksPlugin and NamedModulesPlugin.
productionSets process.env.NODE_ENV to value production. Enables FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin and UglifyJsPlugin.
noneOpts out of any default optimization options

启动

package.json "scripts" 中添加npm脚本,从而快捷运行开发服务器 | 打包生产环境代码

//package.json
{
  "name": "webpack-demo",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack-dev-server --open --config webpack.dev.js",
    "build": "webpack --config webpack.prod.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "clean-webpack-plugin": "^0.1.19",
    "html-webpack-plugin": "^3.2.0",
    "webpack": "^4.15.1",
    "webpack-cli": "^3.0.8",
    "webpack-dev-server": "^3.1.4",
    "webpack-merge": "^4.1.3"
  }
}

"start": "webpack-dev-server --open --config webpack.dev.js",

webpack-dev-server    启动开发服务器   
--open    打开浏览器
--config webpack.dev.js    设置运行此脚本时执行的配置文件为webpack.dev.js

"build": "webpack --config webpack.prod.js"

webpack 启动webpack
--config webpack.prod.js   设置运行此脚本时执行的配置文件为webpack.prod.js
执行 npm start

此时应该可以看到 Hello webpack4!

执行 npm run build

项目文件夹中自动生成打包后的文件目录(输出文件夹dist)

 webpack-demo
  |- package.json
  |- webpack.common.js
  |- webpack.dev.js
  |- webpack.prod.js
  |- /src
    |- index.js
  |- /dist 
      | - index.html
      | - app.bundle.js
  |- /node_modules

使用sourcemap

sourcemap 能实现打包后的运行代码与源代码的映射,帮助我们debug到原始开发代码。

///webpack.dev.js
  module.exports = merge(common,{
    devtool: 'cheap-module-eval-source-map',
    ...
  });

大多数时候开发环境用'cheap-module-eval-source-map'是最好的选择,想要完整的功能又不介意构建速度的话就直接用'source-map'。具体的配置项很多,可以是eval,source-map,cheap,module,inline的任意组合。
具体每个参数的作用请查阅官方api:https://webpack.js.org/config...
也可参考这篇文章https://segmentfault.com/a/11... 这里不做详述。

代码分离

把代码分离到不同的 bundle 中,可以按需加载或并行加载这些文件。可用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。

三种常用的代码分离方法:

1.入口起点:使用 entry 配置手动地分离代码。

先在src文件夹添加一个文件another-module.js

//another-module.js
  import _ from 'lodash';
  console.log(
    _.join(['Another', 'module', 'loaded!'], ' ')
  ); 

修改index.js

//index.js
  import _ from 'lodash';
  console.log(
    _.join(['index', 'module', 'loaded!'], ' ')
  );

用到了lodash,安装下依赖

npm i lodash -S

修改webpack.common.js中entry和output配置

//webpack.common.js
  module.exports = {
    entry: {
      index: './src/index.js',
      another: './src/another-module.js'
    },
    output: {
      filename: '[name].bundle.js',            //根据入口文件名来定义输出文件名
      path: path.resolve(__dirname, 'dist')
    }
};
执行 npm run build

将生成如下构建结果:

Hash: 66f57fffc46778f3b145
Version: webpack 4.16.0
Time: 2966ms
            Asset       Size  Chunks             Chunk Names
another.bundle.js   70.4 KiB       0  [emitted]  another
  index.bundle.js   70.4 KiB       1  [emitted]  index
       index.html  251 bytes          [emitted]  
[1] (webpack)/buildin/module.js 497 bytes {0} {1} [built]
[2] (webpack)/buildin/global.js 489 bytes {0} {1} [built]
[3] ./src/another-module.js 86 bytes {0} [built]
[4] ./src/index.js 83 bytes {1} [built]
    + 1 hidden module
Child html-webpack-plugin for "index.html":
     1 asset
    [0] (webpack)/buildin/module.js 497 bytes {0} [built]
    [1] (webpack)/buildin/global.js 489 bytes {0} [built]
        + 2 hidden modules

存在的问题:

  • 如果入口 chunks 之间包含重复的模块,那些重复模块都会被引入到各个 bundle 中。
  • 不够灵活,不能将核心应用程序逻辑动态地拆分代码。

以上两点中,第一点对我们的示例来说无疑是个问题,index.js 和another-module.js中都引入了 lodash,这样就在两个 bundle 中造成重复引用。接着,我们通过使用 SplitChunks 来移除重复的模块。

2.防止重复:使用SplitChunks 去重和分离 chunk。webpack4 之前版本用的是CommonsChunkPlugin

//webpack.common.js
  const path = require('path');
  module.exports = {
    entry:  {
      index: './src/index.js',
      another: './src/another-module.js'
    },
    plugins: [
      new CleanWebpackPlugin(['dist']),
      new HtmlWebpackPlugin({
        title: 'Production'
      })
    ],
    output: {
      filename: '[name].bundle.js',            //根据入口文件名来定义输出文件名
      path: path.resolve(__dirname, 'dist')
    },
+   optimization: {
+     splitChunks: {
+       chunks: 'all'
+     }
+   }
  };
再次执行npm run build查看效果
...

vendors~another~index.bundle.js   69.5 KiB       0  [emitted]  vendors~another~index
              another.bundle.js   1.54 KiB       1  [emitted]  another
                index.bundle.js   1.54 KiB       2  [emitted]  index
                
...

观察打包后文件大小,可以看到index.bundle.jsanother.bundle.js中已经移除了重复的依赖模块。lodash 被分离到单独的vendors~another~index.bundle.jschunk中。

3.动态导入:通过模块的内联函数调用来分离代码。
略~。~

分离css

需要用到插件mini-css-extract-plugin,这个插件会将提取css到单独的文件,根据每个包含css的js文件创建一个css文件,因此,你的样式将不再内嵌到 JS bundle 中。如果你的样式文件大小较大,这会做更快提前加载,因为 CSS bundle 会跟 JS bundle 并行加载。同时还支持按需加载css和SourceMaps.

相较于旧版extract-text-webpack-plugin插件,mini-css-extract-plugin的优势有

  • 异步加载
  • 没有重复的编译
  • 更容易使用
  • Specific to CSS
  • 支持热更新
npm i mini-css-extract-plugin -D
//webpack.common.js
  const MiniCssExtractPlugin = require("mini-css-extract-plugin");
  module.exports = {
    ...
    plugins: [
      new MiniCssExtractPlugin({
        filename: "[name].css",
      })
    ],
    module: {
      rules: [
        {
          test: /\.css$/,
          use: [
            {
              loader: MiniCssExtractPlugin.loader,
              options: {
                publicPath: '../'             //可以配置输出的css文件路径
              }
            },
            "css-loader"
          ]
        }
      ]
    }
    ...
  }

注意,这个插件不兼容style-loader (用于以<style>标签形式将css-loader内部样式注入到HTML页面)。

如果想在开发环境下使用style-loader,在生产环境分离css文件,可以这么配置:

//webpack.common.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const devMode = process.env.NODE_ENV !== 'production'

module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css',
    })
  ],
  module: {
    rules: [
      {
        test: /\.(sa|sc|c)ss$/,
        use: [
          devMode ? 'style-loader' : MiniCssExtractPlugin.loader,
          'css-loader',
          {
            loader:"postcss-loader",    //本文未用到此loader...
            options: {           // 如果没有options这个选项将会报错 No PostCSS Config found
                plugins: (loader) => []
            }
          },
          'sass-loader',
        ],
      }
    ]
  }
}

很明显我们需要先安装处理样式文件的各种loader

npm i style-loader css-loader postcss-loader node-sass sass-loader -D

这里有个问题,node环境变量process.env.NODE_ENV在webpack.config中其实是undefined,之前提及的mode配置会自动定义这个环境变量,但只能在打包后的js中取到,如何在webpack的配置文件中获取这个值呢,需要引入cross-env

npm i cross-env -D

然后在package.json的脚本命令中指定环境变量

"start": "cross-env NODE_ENV=development webpack-dev-server --open --config webpack.dev.js",
"build": "cross-env NODE_ENV=production  webpack --config webpack.prod.js"

可自行添加css文件,在js中import,执行npm run build查看效果

当然也可以不获取process.env.NODE_ENV来区分环境,在dev.js和prod.js分别配置处理样式文件的rule就行了,这也是最开始我们分开写开发环境和生产环境的webpack配置文件的原因。这里提及只是方便从低版本webpack迁移到4.x。

在单个文件中提取所有CSS
配合optimization.splitChunks.cacheGroups使用

//webpack.common.js
  optimization: {
    splitChunks: {
      cacheGroups: {
        styles: {
          name: 'styles',
          test: /\.css$/,
          chunks: 'all',
          enforce: true
        }
      }
    }
  }

会额外生成一个styles.bundle.js

按照入口JS来分离css

//webpack.common.js
...

function recursiveIssuer(m) {
  if (m.issuer) {
    return recursiveIssuer(m.issuer);
  } else if (m.name) {
    return m.name;
  } else {
    return false;
  }
}

module.exports = {
  entry:  {
    index: './src/index.js',
    another: './src/another-module.js'
  },

  ...

  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {                                //分离第三方库
          test: /[\\/]node_modules[\\/]/,
          name: 'lodash',
          chunks: 'all'
        },
        indexStyles: {
          name: 'index',                        
          test: (m,c,entry = 'index') => m.constructor.name === 'CssModule' && recursiveIssuer(m) === entry,
          chunks: 'all',
          enforce: true
        },
        otherStyles: {
          name: 'another',
          test: (m,c,entry = 'another') => m.constructor.name === 'CssModule' && recursiveIssuer(m) === entry,
          chunks: 'all',
          enforce: true
        }
      }
    }
  }
};

压缩css

webpack5可能会内置CSS压缩,webpack4需要使用像optimize-css-assets-webpack-plugin这样的插件。有个问题是设置optimization.minimizer后,会覆盖上文提到的mode配置项提供的默认值,因此需要同时使用JS压缩插件UglifyJsPlugin

npm i optimize-css-assets-webpack-plugin uglifyjs-webpack-plugin -D
//webpack.prod.js
const merge = require('webpack-merge');
const common = require('./webpack.config.js');

const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");

module.exports = merge(common,{
    mode: "production",
    optimization: {
        minimizer: [
          new UglifyJsPlugin({
            cache: true,
            parallel: true,
            sourceMap: true // set to true if you want JS source maps
          }),
          new OptimizeCSSAssetsPlugin({})
        ],
    }
});

缓存

我们都知道浏览器获取资源是比较耗费时间的,所以它会使用一种名为 缓存 的技术。通过命中缓存,以降低网络流量,使网站加载速度更快。如果我们在部署新版本时不更改资源的文件名,浏览器就可能会认为它没有被更新,就会使用它的缓存版本。因此确保 webpack 编译生成的文件能够被客户端缓存,而在文件内容变化后,能够请求到新的文件是很有必要的。

通过使用 output.filename 进行文件名替换,可以确保浏览器获取到修改后的文件。[hash]替换可以用于在文件名中包含一个构建相关的hash,但是更好的方式是使用[chunkhash]替换,在文件名中包含一个 chunk相关的hash。遗憾的是chunkhash和热更新不兼容,所以开发环境和生产环境要分开配置。

//webpack.common.js
  ...
  output: {
      filename: devMode ? '[name].[hash:8].js': '[name].[chunkhash:8].js',       //数字8表示取hash标识符的前八位
      chunkFilename: devMode ? '[name].[hash:8].js': '[name].[chunkhash:8].js',  //异步模块的文件输出名
      path: path.resolve(__dirname, 'dist')
  },
  ...

关于[hash][chunkhash]的区别,简单来说,[hash]是编译(compilation)后的hash值,compilation对象代表某个版本的资源对应的编译进程。项目中任何一个文件改动,webpack就会重新创建compilation对象,然后计算新的compilation的hash值,所有的编译输出文件名都会使用相同的hash指纹,改一个就一起变。而[chunkhash]是根据具体模块文件的内容计算所得的hash值,某个文件的改动只会影响它本身的hash指纹,不会影响其他文件。

上文代码分离一节中已经提到了如何将第三方库(比如lodash或react)提取到单独的vendor chunk文件中,因为它们很少像本地的源代码那样频繁修改。利用客户端的长效缓存机制,可以消除请求,减少向服务器获取资源,同时还能保证客户端代码和服务器端代码版本一致。
除了第三方库,webpack在入口模块中,包含了某些样板(boilerplate),确切来说就是runtimemanifest。即webpack运行时的引导代码,这部分代码我们也将它单独提取出来。

//webpack.common.js
  ...
  optimization: {
    runtimeChunk: 'single',        //分离webpack运行时的引导代码
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    }
  }
  ...

我们删掉another-module.js,修改index.js如下

///index.js
import _ from 'lodash';

function component() {
    var element = document.createElement('div');

    // Lodash, now imported by this script
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');

    return element;
}

document.body.appendChild(component());
执行npm run build

产生以下输出:

Hash: 131e8681e4403392cb5d
Version: webpack 4.16.1
Time: 744ms
              Asset       Size  Chunks             Chunk Names
  index.5bc56cae.js  260 bytes       0  [emitted]  index
vendors.5d8f5a63.js   69.5 KiB       1  [emitted]  vendors
runtime.eb6eb2fb.js   1.42 KiB       2  [emitted]  runtime
         index.html  316 bytes          [emitted]  
[1] ./src/index.js 253 bytes {0} [built]
[2] (webpack)/buildin/global.js 489 bytes {1} [built]
[3] (webpack)/buildin/module.js 497 bytes {1} [built]
    + 1 hidden module
Child html-webpack-plugin for "index.html":
     1 asset
    [2] (webpack)/buildin/global.js 489 bytes {0} [built]
    [3] (webpack)/buildin/module.js 497 bytes {0} [built]
        + 2 hidden modules

可以看到编译出的文件名后加上了hash,运行时的引导代码也被单独提取出来了。

接着添加一个print.js

///print.js
export default function print(text) {
   console.log(text);
};

修改index.js

///index.js
  import _ from 'lodash';
+ import Print from './print';

  function component() {
    var element = document.createElement('div');

    // Lodash, now imported by this script
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+   element.onclick = Print.bind(null, 'Hello webpack!');

    return element;
  }

  document.body.appendChild(component());
再执行npm run build

构建结果如下:

Hash: a710a54674ea8d4b3263
Version: webpack 4.16.1
Time: 3328ms
              Asset       Size  Chunks             Chunk Names
  index.15466585.js  327 bytes       0  [emitted]  index
vendors.7bde7828.js   69.5 KiB       1  [emitted]  vendors
runtime.eb6eb2fb.js   1.42 KiB       2  [emitted]  runtime
         index.html  316 bytes          [emitted]  
[1] (webpack)/buildin/global.js 489 bytes {1} [built]
[2] (webpack)/buildin/module.js 497 bytes {1} [built]
[3] ./src/index.js + 1 modules 406 bytes {0} [built]
    | ./src/index.js 337 bytes [built]
    | ./src/print.js 64 bytes [built]
    + 1 hidden module
Child html-webpack-plugin for "index.html":
     1 asset
    [2] (webpack)/buildin/global.js 489 bytes {0} [built]
    [3] (webpack)/buildin/module.js 497 bytes {0} [built]
        + 2 hidden modules

我们期望的是,只有 index bundle 的 hash 发生变化,然而vendors也跟着变了。这是因为每个 module.id 会基于默认的解析顺序(resolve order)进行增量。也就是说,当解析顺序发生变化,ID 也会随之改变。(官方文档里说runtime的hash也发生了变化,这里并未出现)
可以使用两个插件来解决这个问题。一是NamedModulesPlugin,将使用模块的路径而不是数字标识符。此插件有助于在开发过程中输出结果的可读性,但执行时间会长一些。二是使用webpack内置插件HashedModuleIdsPlugin,推荐用于生产环境构建:

  const webpack = require('webpack');
  ...
  module.exports = {
    ...
    plugins: [
      ...
      new webpack.HashedModuleIdsPlugin(),
      ...
    ],
    ...
  };

接下来,我们可以随意修改index.js的代码或者增删print.js,再进行构建查看hash的变化。

关于css缓存
假如index.js引用了一个index.css文件,它们会共用相同的chunkhash值。这时如果index.js更改了代码,index.css文件就算内容没有任何改变也会重复构建。
我们可以使用MiniCssExtractPlugin里的contenthash,保证css文件所处的模块里只要css文件内容不变,其本身就不会重复构建。

new MiniCssExtractPlugin({
  filename: "[name].[contenthash:8].css",
  chunkFilename: "[name].[contenthash:8].css"
}),

这样基本就完成了webpack的缓存配置。

有个小问题是,当修改index.css文件代码,重新构建后index.js的hash值也一起改变了。。。
尝试了下安装插件WebpackMd5Hash可以解决

npm i webpack-md5-hash -D

but,这个插件可能会引发别的bug,好吧这里先不用,后续补充,有兴趣可自行搜索

Babel

完成了基本的配置文件编写与代码分离,开发中需要用babel将旧的浏览器或环境中的es 2015+代码转换为es5。
需要安装一些依赖。

babel-core           //必备的核心库
babel-loader         //webpack loader配置必备
babel-preset-env     //支持es2015、2016、2017,
babel-preset-stage-0 //默认向后支持 stage-1,stage-2,stage-3,
babel-runtime        
babel-plugin-transform-runtime //转译新的API
npm i babel-runtime -S
npm i babel-core babel-loader babel-preset-env babel-preset-stage-0 babel-plugin-transform-runtime  -D

创建.babelrc文件

///.babelrc
{
  "presets": [
    "env",
    "stage-0"
  ],
  "plugins": [
    ["transform-runtime", {
      "helpers": false, //建议为false
      "polyfill": false, //是否切换新的内置插件(Promise,Set,Map等)为使用非全局污染的 polyfill,根据你的网站兼容性情况来看,开启会增加很多额外的代码
      "regenerator": true //是否切换 generator 函数为不污染全局作用域的 regenerator runtime。
    }],
  ]
}

关于 babel-polyfill 与 babel-plugin-transform-runtime

babel 可以转译新的 JavaScript 语法,但并不会转化BOM里面不兼容的API比如Promise,Set,Symbol,Array.from,async 等。这时就需要 polyfill(软垫片) 来转化这些API

babel-polyfill会仿效一个完整的 ES2015+ 环境,这样你就可以使用新的内置对象比如 Promise 或WeakMap, 静态方法比如 Array.from 或者 Object.assign, 实例方法比如 Array.prototype.includes 和生成器函数(提供 regenerator 插件)。babel-polyfill缺点是它通过改写全局prototype的方式实现,会污染全局对象所以不适合第三方库的开发,且打包后代码冗余量比较大,我们可能不需要用到所有的新API,对于现代浏览器有些也不需要polyfill。

babel-plugin-transform-runtime 依赖babel-runtime,babel编译es6到es5的过程中,babel-plugin-transform-runtime会自动polyfill es5不支持的特性,这些polyfill包就是在babel-runtime这个包里(这就是为啥babel-runtime 需要作为生产依赖引入(使用 --save))。transform-runtime优点是不会污染全局变量,多次使用只会打包一次,并且统一按需引入依赖,无重复、多余引入。缺点是例如"foobar".includes("foo")等实例方法将不起作用。

React

以react开发为例,如果是搭建新的项目,可以直接安装官方脚手架create-react-app或者使用阿里的开源ui框架 Ant Design
这里仅仅提一下如何在webpack中配置react开发环境

npm install react react-dom -S

还需要安装

babel-plugin-transform-decorators-legacy //支持修饰符语法 @connect
babel-preset-react   //解析react语法
react-hot-loader     //react热更新需要在babelrc做配置
///.babelrc
  {
  "presets": [
    "env",
    "react",
    "stage-0"
  ],
  "plugins": [
    ["transform-runtime", {
      "helpers": false, //建议为false
      "polyfill": false, //是否开始polyfill,根据网站兼容性决定是否开启
      "regenerator": true 
    }],
    "react-hot-loader/babel",     //react热更新插件
    "transform-decorators-legacy" //修饰符语法转换插件
  ]
}

如果之前webpack-dev-server配置正确,这时只要把你的根组件标记为热导出,就能启用react热更新

///index.js
  import React from 'react';
  import { hot } from 'react-hot-loader';
     
  const App = () => <div>Hello World!</div>
     
  export default hot(module)(App);

别忘了配置babel-loader

///webpack.common.js
  module: {
      rules: [{
          test: /\.jsx?$/,
          use: 'babel-loader'
      }]
  }

未完待续,容老夫喝口水先...

查看原文

赞 25 收藏 14 评论 4

lxtmx 发布了文章 · 2019-06-11

webpack4.x配置指南

简介

鉴于webpack更新太快,总结下基础配置方法,理解有限,仅做抛砖引玉之用。

起步

初始化配置文件 package.json并安装webpack

mkdir webpack-demo && cd webpack-demo
npm init -y    //-y 初始化选项默认为 yes      
npm i webpack webpack-cli -D // -D 即 -save-dev    版本4.x以上需要安装webpack-cli

创建以下目录结构、文件和内容:

webpack-demo
  |- package.json
+ |- /src
+   |- index.js
//index.js
document.write("Hello webpack4!");

创建webpack配置文件

编写开发环境和生产环境彼此独立的webpack配置文件
先添加三个文件

 webpack-demo
  |- package.json
+ |- webpack.common.js
+ |- webpack.dev.js
+ |- webpack.prod.js
  |- /src
    |- index.js
  |- /node_modules

1.webpack.common.js

用到两个基本的插件

npm i clean-webpack-plugin html-webpack-plugin -D

clean-webpack-plugin:打包时自动清除输出文件夹中未用到的文件;
html-webpack-plugin:打包时会自动生成index.html并替换已有的index.html,bundle.js也会自行添加到 html 中。

  //webpack.common.js
    const path = require('path');
    const { CleanWebpackPlugin } = require('clean-webpack-plugin');  
    const HtmlWebpackPlugin = require('html-webpack-plugin');    
    
    module.exports = {
      entry: './src/index.js',
      plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
          title: 'index'
        })
      ],
      output: {
        filename: 'bundle.js',           
        path: path.resolve(__dirname, 'dist')    //定义输出文件夹dist路径
      }
    };

2.webpack.dev.js

先安装webpack-merge,用以合并通用配置文件与开发环境配置文件

npm i webpack-merge -D

安装开发服务器devServer,作用是修改代码后实时重新加载(刷新浏览器)

npm i webpack-dev-server -D
//webpack.dev.js
  const merge = require('webpack-merge');
  const common = require('./webpack.common.js');
  const webpack = require('webpack');
    
  module.exports = merge(common,{
     devServer: {                //启用开发服务器
       contentBase: './dist',   //告诉服务器从哪提供内容,只有在想要提供静态文件时才需要
       compress: true,          //一切服务都启用gzip 压缩
       host: '0.0.0.0',         //指定使用一个host,可用ip地址访问,没有的话如果别人访问会被禁止。默认localhost。
       port: '9999',            //指定端口号,如省略,默认为”8080“
       hot: true,               //启用模块热替换特性
       inline: true,            //启用内联模式,一段处理实时重载的脚本被插入到bundle中,并且构建消息会出现在浏览器控制台
       historyApiFallback: true,//开发单页应用时有用,依赖于HTML5 history API,设为true时所有跳转将指向index.html
     },
     plugins: [
         new webpack.HotModuleReplacementPlugin(),  //webpack内置的热更新插件
     ],
     mode: 'development'
  });

devServer的更多可选参数-https://www.webpackjs.com/con...

HotModuleReplacementPlugin 模块热替换(Hot Module Replacement)插件,用以在运行时更新发生改变的模块,从而无需进行完全刷新。

3.webpack.prod.js

同样用'webpack-merge'合并通用配置文件与生产环境配置文件

//webpack.prod.js
  const merge = require('webpack-merge');
  const common = require('./webpack.common.js');
    
  module.exports = merge(common,{
    mode: "production"
  });
关于mode

此时你可能会注意到配置文件中有个mode项,webpack4中新加,作用如下:

  • --mode production 生产环境

    不需要像旧版本一样定义node环境变量
    new webpack.DefinePlugin({"process.env.NODE_ENV":JSON.stringify("production") })

    ps:许多 library 将通过与 process.env.NODE_ENV 环境变量关联,以决定 library 中应该引用哪些内容。当使用 process.env.NODE_ENV === 'production' 时,一些 library 可能针对具体用户的环境进行代码优化,从而删除或添加一些重要代码。
    

    自动开启一些插件,如:
    uglifyjs-webpack-plugin js代码压缩(所以无需再单独使用)
    NoEmitOnErrorsPlugin 编译出错时跳过输出,以确保输出资源不包含错误
    ModuleConcatenationPlugin webpack3 添加的作用域提升(Scope Hoisting)

  • --mode development 开发环境

    自行定义node环境变量为development
    new webpack.DefinePlugin({"process.env.NODE_ENV":JSON.stringify("development") })
    使用 eval 构建 module, 提升增量构建速度
    自动开启一些插件,如
    NamedModulesPlugin 使用模块热替换(HMR)时会显示模块的相对路径

具体描述:

OptionDescription
developmentSets process.env.NODE_ENV to value development. Enables NamedChunksPlugin and NamedModulesPlugin.
productionSets process.env.NODE_ENV to value production. Enables FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin and UglifyJsPlugin.
noneOpts out of any default optimization options

启动

package.json "scripts" 中添加npm脚本,从而快捷运行开发服务器 | 打包生产环境代码

//package.json
{
  "name": "webpack-demo",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack-dev-server --open --config webpack.dev.js",
    "build": "webpack --config webpack.prod.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "clean-webpack-plugin": "^0.1.19",
    "html-webpack-plugin": "^3.2.0",
    "webpack": "^4.15.1",
    "webpack-cli": "^3.0.8",
    "webpack-dev-server": "^3.1.4",
    "webpack-merge": "^4.1.3"
  }
}

"start": "webpack-dev-server --open --config webpack.dev.js",

webpack-dev-server    启动开发服务器   
--open    打开浏览器
--config webpack.dev.js    设置运行此脚本时执行的配置文件为webpack.dev.js

"build": "webpack --config webpack.prod.js"

webpack 启动webpack
--config webpack.prod.js   设置运行此脚本时执行的配置文件为webpack.prod.js
执行 npm start

此时应该可以看到 Hello webpack4!

执行 npm run build

项目文件夹中自动生成打包后的文件目录(输出文件夹dist)

 webpack-demo
  |- package.json
  |- webpack.common.js
  |- webpack.dev.js
  |- webpack.prod.js
  |- /src
    |- index.js
  |- /dist 
      | - index.html
      | - app.bundle.js
  |- /node_modules

使用sourcemap

sourcemap 能实现打包后的运行代码与源代码的映射,帮助我们debug到原始开发代码。

///webpack.dev.js
  module.exports = merge(common,{
    devtool: 'cheap-module-eval-source-map',
    ...
  });

大多数时候开发环境用'cheap-module-eval-source-map'是最好的选择,想要完整的功能又不介意构建速度的话就直接用'source-map'。具体的配置项很多,可以是eval,source-map,cheap,module,inline的任意组合。
具体每个参数的作用请查阅官方api:https://webpack.js.org/config...
也可参考这篇文章https://segmentfault.com/a/11... 这里不做详述。

代码分离

把代码分离到不同的 bundle 中,可以按需加载或并行加载这些文件。可用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。

三种常用的代码分离方法:

1.入口起点:使用 entry 配置手动地分离代码。

先在src文件夹添加一个文件another-module.js

//another-module.js
  import _ from 'lodash';
  console.log(
    _.join(['Another', 'module', 'loaded!'], ' ')
  ); 

修改index.js

//index.js
  import _ from 'lodash';
  console.log(
    _.join(['index', 'module', 'loaded!'], ' ')
  );

用到了lodash,安装下依赖

npm i lodash -S

修改webpack.common.js中entry和output配置

//webpack.common.js
  module.exports = {
    entry: {
      index: './src/index.js',
      another: './src/another-module.js'
    },
    output: {
      filename: '[name].bundle.js',            //根据入口文件名来定义输出文件名
      path: path.resolve(__dirname, 'dist')
    }
};
执行 npm run build

将生成如下构建结果:

Hash: 66f57fffc46778f3b145
Version: webpack 4.16.0
Time: 2966ms
            Asset       Size  Chunks             Chunk Names
another.bundle.js   70.4 KiB       0  [emitted]  another
  index.bundle.js   70.4 KiB       1  [emitted]  index
       index.html  251 bytes          [emitted]  
[1] (webpack)/buildin/module.js 497 bytes {0} {1} [built]
[2] (webpack)/buildin/global.js 489 bytes {0} {1} [built]
[3] ./src/another-module.js 86 bytes {0} [built]
[4] ./src/index.js 83 bytes {1} [built]
    + 1 hidden module
Child html-webpack-plugin for "index.html":
     1 asset
    [0] (webpack)/buildin/module.js 497 bytes {0} [built]
    [1] (webpack)/buildin/global.js 489 bytes {0} [built]
        + 2 hidden modules

存在的问题:

  • 如果入口 chunks 之间包含重复的模块,那些重复模块都会被引入到各个 bundle 中。
  • 不够灵活,不能将核心应用程序逻辑动态地拆分代码。

以上两点中,第一点对我们的示例来说无疑是个问题,index.js 和another-module.js中都引入了 lodash,这样就在两个 bundle 中造成重复引用。接着,我们通过使用 SplitChunks 来移除重复的模块。

2.防止重复:使用SplitChunks 去重和分离 chunk。webpack4 之前版本用的是CommonsChunkPlugin

//webpack.common.js
  const path = require('path');
  module.exports = {
    entry:  {
      index: './src/index.js',
      another: './src/another-module.js'
    },
    plugins: [
      new CleanWebpackPlugin(['dist']),
      new HtmlWebpackPlugin({
        title: 'Production'
      })
    ],
    output: {
      filename: '[name].bundle.js',            //根据入口文件名来定义输出文件名
      path: path.resolve(__dirname, 'dist')
    },
+   optimization: {
+     splitChunks: {
+       chunks: 'all'
+     }
+   }
  };
再次执行npm run build查看效果
...

vendors~another~index.bundle.js   69.5 KiB       0  [emitted]  vendors~another~index
              another.bundle.js   1.54 KiB       1  [emitted]  another
                index.bundle.js   1.54 KiB       2  [emitted]  index
                
...

观察打包后文件大小,可以看到index.bundle.jsanother.bundle.js中已经移除了重复的依赖模块。lodash 被分离到单独的vendors~another~index.bundle.jschunk中。

3.动态导入:通过模块的内联函数调用来分离代码。
略~。~

分离css

需要用到插件mini-css-extract-plugin,这个插件会将提取css到单独的文件,根据每个包含css的js文件创建一个css文件,因此,你的样式将不再内嵌到 JS bundle 中。如果你的样式文件大小较大,这会做更快提前加载,因为 CSS bundle 会跟 JS bundle 并行加载。同时还支持按需加载css和SourceMaps.

相较于旧版extract-text-webpack-plugin插件,mini-css-extract-plugin的优势有

  • 异步加载
  • 没有重复的编译
  • 更容易使用
  • Specific to CSS
  • 支持热更新
npm i mini-css-extract-plugin -D
//webpack.common.js
  const MiniCssExtractPlugin = require("mini-css-extract-plugin");
  module.exports = {
    ...
    plugins: [
      new MiniCssExtractPlugin({
        filename: "[name].css",
      })
    ],
    module: {
      rules: [
        {
          test: /\.css$/,
          use: [
            {
              loader: MiniCssExtractPlugin.loader,
              options: {
                publicPath: '../'             //可以配置输出的css文件路径
              }
            },
            "css-loader"
          ]
        }
      ]
    }
    ...
  }

注意,这个插件不兼容style-loader (用于以<style>标签形式将css-loader内部样式注入到HTML页面)。

如果想在开发环境下使用style-loader,在生产环境分离css文件,可以这么配置:

//webpack.common.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const devMode = process.env.NODE_ENV !== 'production'

module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css',
    })
  ],
  module: {
    rules: [
      {
        test: /\.(sa|sc|c)ss$/,
        use: [
          devMode ? 'style-loader' : MiniCssExtractPlugin.loader,
          'css-loader',
          {
            loader:"postcss-loader",    //本文未用到此loader...
            options: {           // 如果没有options这个选项将会报错 No PostCSS Config found
                plugins: (loader) => []
            }
          },
          'sass-loader',
        ],
      }
    ]
  }
}

很明显我们需要先安装处理样式文件的各种loader

npm i style-loader css-loader postcss-loader node-sass sass-loader -D

这里有个问题,node环境变量process.env.NODE_ENV在webpack.config中其实是undefined,之前提及的mode配置会自动定义这个环境变量,但只能在打包后的js中取到,如何在webpack的配置文件中获取这个值呢,需要引入cross-env

npm i cross-env -D

然后在package.json的脚本命令中指定环境变量

"start": "cross-env NODE_ENV=development webpack-dev-server --open --config webpack.dev.js",
"build": "cross-env NODE_ENV=production  webpack --config webpack.prod.js"

可自行添加css文件,在js中import,执行npm run build查看效果

当然也可以不获取process.env.NODE_ENV来区分环境,在dev.js和prod.js分别配置处理样式文件的rule就行了,这也是最开始我们分开写开发环境和生产环境的webpack配置文件的原因。这里提及只是方便从低版本webpack迁移到4.x。

在单个文件中提取所有CSS
配合optimization.splitChunks.cacheGroups使用

//webpack.common.js
  optimization: {
    splitChunks: {
      cacheGroups: {
        styles: {
          name: 'styles',
          test: /\.css$/,
          chunks: 'all',
          enforce: true
        }
      }
    }
  }

会额外生成一个styles.bundle.js

按照入口JS来分离css

//webpack.common.js
...

function recursiveIssuer(m) {
  if (m.issuer) {
    return recursiveIssuer(m.issuer);
  } else if (m.name) {
    return m.name;
  } else {
    return false;
  }
}

module.exports = {
  entry:  {
    index: './src/index.js',
    another: './src/another-module.js'
  },

  ...

  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {                                //分离第三方库
          test: /[\\/]node_modules[\\/]/,
          name: 'lodash',
          chunks: 'all'
        },
        indexStyles: {
          name: 'index',                        
          test: (m,c,entry = 'index') => m.constructor.name === 'CssModule' && recursiveIssuer(m) === entry,
          chunks: 'all',
          enforce: true
        },
        otherStyles: {
          name: 'another',
          test: (m,c,entry = 'another') => m.constructor.name === 'CssModule' && recursiveIssuer(m) === entry,
          chunks: 'all',
          enforce: true
        }
      }
    }
  }
};

压缩css

webpack5可能会内置CSS压缩,webpack4需要使用像optimize-css-assets-webpack-plugin这样的插件。有个问题是设置optimization.minimizer后,会覆盖上文提到的mode配置项提供的默认值,因此需要同时使用JS压缩插件UglifyJsPlugin

npm i optimize-css-assets-webpack-plugin uglifyjs-webpack-plugin -D
//webpack.prod.js
const merge = require('webpack-merge');
const common = require('./webpack.config.js');

const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");

module.exports = merge(common,{
    mode: "production",
    optimization: {
        minimizer: [
          new UglifyJsPlugin({
            cache: true,
            parallel: true,
            sourceMap: true // set to true if you want JS source maps
          }),
          new OptimizeCSSAssetsPlugin({})
        ],
    }
});

缓存

我们都知道浏览器获取资源是比较耗费时间的,所以它会使用一种名为 缓存 的技术。通过命中缓存,以降低网络流量,使网站加载速度更快。如果我们在部署新版本时不更改资源的文件名,浏览器就可能会认为它没有被更新,就会使用它的缓存版本。因此确保 webpack 编译生成的文件能够被客户端缓存,而在文件内容变化后,能够请求到新的文件是很有必要的。

通过使用 output.filename 进行文件名替换,可以确保浏览器获取到修改后的文件。[hash]替换可以用于在文件名中包含一个构建相关的hash,但是更好的方式是使用[chunkhash]替换,在文件名中包含一个 chunk相关的hash。遗憾的是chunkhash和热更新不兼容,所以开发环境和生产环境要分开配置。

//webpack.common.js
  ...
  output: {
      filename: devMode ? '[name].[hash:8].js': '[name].[chunkhash:8].js',       //数字8表示取hash标识符的前八位
      chunkFilename: devMode ? '[name].[hash:8].js': '[name].[chunkhash:8].js',  //异步模块的文件输出名
      path: path.resolve(__dirname, 'dist')
  },
  ...

关于[hash][chunkhash]的区别,简单来说,[hash]是编译(compilation)后的hash值,compilation对象代表某个版本的资源对应的编译进程。项目中任何一个文件改动,webpack就会重新创建compilation对象,然后计算新的compilation的hash值,所有的编译输出文件名都会使用相同的hash指纹,改一个就一起变。而[chunkhash]是根据具体模块文件的内容计算所得的hash值,某个文件的改动只会影响它本身的hash指纹,不会影响其他文件。

上文代码分离一节中已经提到了如何将第三方库(比如lodash或react)提取到单独的vendor chunk文件中,因为它们很少像本地的源代码那样频繁修改。利用客户端的长效缓存机制,可以消除请求,减少向服务器获取资源,同时还能保证客户端代码和服务器端代码版本一致。
除了第三方库,webpack在入口模块中,包含了某些样板(boilerplate),确切来说就是runtimemanifest。即webpack运行时的引导代码,这部分代码我们也将它单独提取出来。

//webpack.common.js
  ...
  optimization: {
    runtimeChunk: 'single',        //分离webpack运行时的引导代码
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    }
  }
  ...

我们删掉another-module.js,修改index.js如下

///index.js
import _ from 'lodash';

function component() {
    var element = document.createElement('div');

    // Lodash, now imported by this script
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');

    return element;
}

document.body.appendChild(component());
执行npm run build

产生以下输出:

Hash: 131e8681e4403392cb5d
Version: webpack 4.16.1
Time: 744ms
              Asset       Size  Chunks             Chunk Names
  index.5bc56cae.js  260 bytes       0  [emitted]  index
vendors.5d8f5a63.js   69.5 KiB       1  [emitted]  vendors
runtime.eb6eb2fb.js   1.42 KiB       2  [emitted]  runtime
         index.html  316 bytes          [emitted]  
[1] ./src/index.js 253 bytes {0} [built]
[2] (webpack)/buildin/global.js 489 bytes {1} [built]
[3] (webpack)/buildin/module.js 497 bytes {1} [built]
    + 1 hidden module
Child html-webpack-plugin for "index.html":
     1 asset
    [2] (webpack)/buildin/global.js 489 bytes {0} [built]
    [3] (webpack)/buildin/module.js 497 bytes {0} [built]
        + 2 hidden modules

可以看到编译出的文件名后加上了hash,运行时的引导代码也被单独提取出来了。

接着添加一个print.js

///print.js
export default function print(text) {
   console.log(text);
};

修改index.js

///index.js
  import _ from 'lodash';
+ import Print from './print';

  function component() {
    var element = document.createElement('div');

    // Lodash, now imported by this script
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+   element.onclick = Print.bind(null, 'Hello webpack!');

    return element;
  }

  document.body.appendChild(component());
再执行npm run build

构建结果如下:

Hash: a710a54674ea8d4b3263
Version: webpack 4.16.1
Time: 3328ms
              Asset       Size  Chunks             Chunk Names
  index.15466585.js  327 bytes       0  [emitted]  index
vendors.7bde7828.js   69.5 KiB       1  [emitted]  vendors
runtime.eb6eb2fb.js   1.42 KiB       2  [emitted]  runtime
         index.html  316 bytes          [emitted]  
[1] (webpack)/buildin/global.js 489 bytes {1} [built]
[2] (webpack)/buildin/module.js 497 bytes {1} [built]
[3] ./src/index.js + 1 modules 406 bytes {0} [built]
    | ./src/index.js 337 bytes [built]
    | ./src/print.js 64 bytes [built]
    + 1 hidden module
Child html-webpack-plugin for "index.html":
     1 asset
    [2] (webpack)/buildin/global.js 489 bytes {0} [built]
    [3] (webpack)/buildin/module.js 497 bytes {0} [built]
        + 2 hidden modules

我们期望的是,只有 index bundle 的 hash 发生变化,然而vendors也跟着变了。这是因为每个 module.id 会基于默认的解析顺序(resolve order)进行增量。也就是说,当解析顺序发生变化,ID 也会随之改变。(官方文档里说runtime的hash也发生了变化,这里并未出现)
可以使用两个插件来解决这个问题。一是NamedModulesPlugin,将使用模块的路径而不是数字标识符。此插件有助于在开发过程中输出结果的可读性,但执行时间会长一些。二是使用webpack内置插件HashedModuleIdsPlugin,推荐用于生产环境构建:

  const webpack = require('webpack');
  ...
  module.exports = {
    ...
    plugins: [
      ...
      new webpack.HashedModuleIdsPlugin(),
      ...
    ],
    ...
  };

接下来,我们可以随意修改index.js的代码或者增删print.js,再进行构建查看hash的变化。

关于css缓存
假如index.js引用了一个index.css文件,它们会共用相同的chunkhash值。这时如果index.js更改了代码,index.css文件就算内容没有任何改变也会重复构建。
我们可以使用MiniCssExtractPlugin里的contenthash,保证css文件所处的模块里只要css文件内容不变,其本身就不会重复构建。

new MiniCssExtractPlugin({
  filename: "[name].[contenthash:8].css",
  chunkFilename: "[name].[contenthash:8].css"
}),

这样基本就完成了webpack的缓存配置。

有个小问题是,当修改index.css文件代码,重新构建后index.js的hash值也一起改变了。。。
尝试了下安装插件WebpackMd5Hash可以解决

npm i webpack-md5-hash -D

but,这个插件可能会引发别的bug,好吧这里先不用,后续补充,有兴趣可自行搜索

Babel

完成了基本的配置文件编写与代码分离,开发中需要用babel将旧的浏览器或环境中的es 2015+代码转换为es5。
需要安装一些依赖。

babel-core           //必备的核心库
babel-loader         //webpack loader配置必备
babel-preset-env     //支持es2015、2016、2017,
babel-preset-stage-0 //默认向后支持 stage-1,stage-2,stage-3,
babel-runtime        
babel-plugin-transform-runtime //转译新的API
npm i babel-runtime -S
npm i babel-core babel-loader babel-preset-env babel-preset-stage-0 babel-plugin-transform-runtime  -D

创建.babelrc文件

///.babelrc
{
  "presets": [
    "env",
    "stage-0"
  ],
  "plugins": [
    ["transform-runtime", {
      "helpers": false, //建议为false
      "polyfill": false, //是否切换新的内置插件(Promise,Set,Map等)为使用非全局污染的 polyfill,根据你的网站兼容性情况来看,开启会增加很多额外的代码
      "regenerator": true //是否切换 generator 函数为不污染全局作用域的 regenerator runtime。
    }],
  ]
}

关于 babel-polyfill 与 babel-plugin-transform-runtime

babel 可以转译新的 JavaScript 语法,但并不会转化BOM里面不兼容的API比如Promise,Set,Symbol,Array.from,async 等。这时就需要 polyfill(软垫片) 来转化这些API

babel-polyfill会仿效一个完整的 ES2015+ 环境,这样你就可以使用新的内置对象比如 Promise 或WeakMap, 静态方法比如 Array.from 或者 Object.assign, 实例方法比如 Array.prototype.includes 和生成器函数(提供 regenerator 插件)。babel-polyfill缺点是它通过改写全局prototype的方式实现,会污染全局对象所以不适合第三方库的开发,且打包后代码冗余量比较大,我们可能不需要用到所有的新API,对于现代浏览器有些也不需要polyfill。

babel-plugin-transform-runtime 依赖babel-runtime,babel编译es6到es5的过程中,babel-plugin-transform-runtime会自动polyfill es5不支持的特性,这些polyfill包就是在babel-runtime这个包里(这就是为啥babel-runtime 需要作为生产依赖引入(使用 --save))。transform-runtime优点是不会污染全局变量,多次使用只会打包一次,并且统一按需引入依赖,无重复、多余引入。缺点是例如"foobar".includes("foo")等实例方法将不起作用。

React

以react开发为例,如果是搭建新的项目,可以直接安装官方脚手架create-react-app或者使用阿里的开源ui框架 Ant Design
这里仅仅提一下如何在webpack中配置react开发环境

npm install react react-dom -S

还需要安装

babel-plugin-transform-decorators-legacy //支持修饰符语法 @connect
babel-preset-react   //解析react语法
react-hot-loader     //react热更新需要在babelrc做配置
///.babelrc
  {
  "presets": [
    "env",
    "react",
    "stage-0"
  ],
  "plugins": [
    ["transform-runtime", {
      "helpers": false, //建议为false
      "polyfill": false, //是否开始polyfill,根据网站兼容性决定是否开启
      "regenerator": true 
    }],
    "react-hot-loader/babel",     //react热更新插件
    "transform-decorators-legacy" //修饰符语法转换插件
  ]
}

如果之前webpack-dev-server配置正确,这时只要把你的根组件标记为热导出,就能启用react热更新

///index.js
  import React from 'react';
  import { hot } from 'react-hot-loader';
     
  const App = () => <div>Hello World!</div>
     
  export default hot(module)(App);

别忘了配置babel-loader

///webpack.common.js
  module: {
      rules: [{
          test: /\.jsx?$/,
          use: 'babel-loader'
      }]
  }

未完待续,容老夫喝口水先...

查看原文

赞 25 收藏 14 评论 4

lxtmx 收藏了文章 · 2019-01-21

RPC vs REST vs GraphQL

写在前面

最近2周的时间由于工作不忙,一直在看有关GraphQL的东西,前后端均有涉及,由于我之前做过后端开发,当时实现的接口的大体是符合RPC风格的接口。后来转做了前端开发,从实现接口者变成了调用接口者,接触最多的当属REST风格的接口。因此在这段学习GraphQL的过程中,并且也尝试使用它以全栈的角度做了一个小项目,在这个过程中,一直在思考它对比前两者在API设计的整体架构体系中的各个指标上,孰优孰劣。

其实在使用和学习的过程中,有很多文章都对比过它们的异同,但是大部分文章并没有从一个相对客观的角度来对比,更多是为了突显一个的优点而刻意指出另外一个的缺点。这让我想到一句话,脱离业务情景谈技术就是耍流氓。

昨天订阅的GraphQL Weekly中推送的一个视频正好是讲关于它们这三者的,于是就点进去看了看,发现质量还是不错的,于是就想整理出来,分享给大家。

原视频地址(油管地址,自备梯子):这里

如果没有梯子的话直接看我整理的东西也可以,我觉的应该都覆盖到视频中所讲的重点内容了。

当然,这些内容如果分开来讲,每一块内容所涉及的东西都够写一本书了,这里仅仅是简单归纳和整理,从宏观的角度来对比它们的异同,从而能够在日后面临技术选型时,有一个更佳明确的决策方向。

RPC

先简单介绍下RPC,它是Remote Procedure Call(远程过程调用)的简称。一般基于RPC协议所设计的接口,是基于网络采用客户端/服务端的模式完成调用接口的。

优点

  • 简单并且易于理解(面向开发者)
  • 轻量级的数据载体
  • 高性能

缺点

  • 对于系统本身耦合性高
  • 因为RPC本身很简单、轻量,因此很容易造成 function explosion

关于RPC的优点其实很好理解,就是因为它性能高同时又很简单,但是我认为这是对于接口提供者来讲的(因为它的高耦合性)。

但是如果从接口调用者的角度来看,高耦合性就变成了缺点,因为高耦合意味着调用者必须要足够了解系统本身的实现才能够完成调用,比如:

  • 调用者需要知道所调用接口的函数名、参数格式、参数顺序、参数名称等等
  • 如果接口提供者(server)要对接口做出一些改变,很容易对接口调用者(client)造成breaking change(违背开闭原则)
  • 一般RPC所暴露接口仅仅会暴露函数的名称和参数等信息,对于函数之间的调用关系无法提供,这意味着调用者必须足够了解系统,从能够知道如何正确的调用这些接口,但是对于接口调用者往往不需要了解过多系统内部实现细节

关于上面的第二点,为了减少breaking change,我之前实现接口的时候一般都会引入版本的概念,就是在暴露接口的方法名中加入版本号,一开始效果确实不错,但是随后就不知不觉的形成了function explosion,和视频中主讲人所举例的例子差不多,贴一下视频中的截图感受一波:

图片描述

REST

当前REST风格的API架构方式已经成了主流解决方案了,相比较RPC,它的主要不同之处在于,它是对于资源(Resource)的模型化而非步骤(Procedure)。

优点

  • 对于系统本身耦合性低,调用者不再需要了解接口内部处理和实现细节
  • 重复使用了一些 http 协议中的已定义好的部分状态动词,增强语义表现力
  • API可以随着时间而不断演进

缺点

  • 缺少约束,缺少简单、统一的规范
  • 有时候 payload 会变的冗余(overload),有时候调用api会比较繁琐(chattiness)
  • 有时候需要发送多条请求已获取数据,在网络带宽较低的场景,往往会造成不好的影响

REST的优点基本解决了RPC中存在的问题,就是解耦,从而使得前后端分离成为可能。接口提供者在修改接口时,不容易造成breaking-change,接口调用者在调用接口时,往往面向数据模型编程,而省去了了解接口本身的时间成本。

但是,我认为REST当前最大的问题在于虽然它利用http的动词约束了接口的暴露方式,同时增强了语义,但是却没有约束接口如何返回数据的最佳实践,总让人感觉只要是返回json格式的接口都可以称作REST。

我在实际工作中,经常会遇到第二条缺点所指出的问题,就是接口返回的数据冗余度很高,但是却缺少我真正需要的数据,因此不得已只能调用其他接口或者直接和后端商议修改接口,并且这种问题会在web端和移动端共用一套接口中被放大。

当前比较好的解决方案就是规范化返回数据的格式,比如json-schema或者自己制定的规范。

GraphQL

GraphQL是近来比较热门的一个技术话题,相比REST和RPC,它汲取了两者的优点,即不面向资源,也不面向过程,而是面向数据查询(ask for exactly what you want)。

同时GraphQL本身需要使用强类型的Schema来对数据模型进行定义,因此相比REST它的约束性更强。

优点

  • 网络开销低,可以在单一请求中获取REST中使用多条请求获取的资源
  • 强类型Schema(约束意味着可以根据规范形成文档、IDE、错误提示等生态工具)
  • 特别适合状数据结构的业务场景(比如好友、流程、组织架构等系统)

缺点

  • 本身的语法相比较REST和RPC均复杂一些
  • 实现方面需要配套 Caching 以解决性能瓶颈
  • 对于 API 的版本控制当前没有完善解决方案(社区的建议是不要使API版本化)
  • 仍然是新鲜事物,很多技术细节仍然处于待验证状态

鉴于GraphQL这两个星期我也仅仅是做了一些简单地使用和了解,仅仅说一下感受。

首先值得肯定的是,在某些程度上确实解决了REST的缺点所带来的问题,同时配套社区建议的各种工具和库,相比使用REST风格,全栈开发体验上升一个台阶。

但是这个看起来很好的东西为什么没有火起来呢?我觉的最主要的原因是因为GraphQL所带来的好处,大部分是对于接口调用者而言的,但是实现这部分的工作却需要接口提供者来完成。

同时GraphQL的最佳实践场景应当是类似像Facebook这样的网站,业务逻辑模型是图状数据结构,比如社交。如果在一些业务逻辑模型相对简单的场景,使用GraphQL确实不如使用REST来得简单明了、直截了当。

另外一方面是GraphQL的使用场景相当灵活,在我自己的调研项目中,我是把它当做一个类似ORM的框架来使用的,在别人的一些文章中,会把它当做一个中间层来做渐进式开发和系统升级。这应当算是另外一个优点。

到底用哪个

下面根据要设计的API类型给予一些技术选型建议。

如果是Management API,这类API的特点如下:

  • 关注于对象与资源
  • 会有多种不同的客户端
  • 需要良好的可发现性和文档

这种情景使用REST + JSON API可能会更好。

如果是Command or Action API,这类API的特点如下:

  • 面向动作或者指令
  • 仅需要简单的交互

这种情况使用RPC就足够了。

如果是Internal Micro Services API,这类API的特点如下:

  • 消息密集型
  • 对系统性能有较高要求

这种情景仍然建议使用RPC

如果是Micro Services API,这类API的特点如下:

  • 消息密集型
  • 期望系统开销较低

这种情景使用RPC或者REST均可。

如果是Data or Mobile API,这类API的特点是:

  • 数据类型是具有图状的特点
  • 希望对于高延迟场景可以有更好的优化

这种场景无疑GraphQL是最好的选择。

写在最后

提供一张表格来总览它们之间在不同指标下的表现:

耦合性约束性复杂度缓存可发现性版本控制
RPC(Function)highmediumlowcustombadhard
REST(Resource)lowlowlowhttpgoodeasy
GraphQL(Query)mediumhighmediumcustomgood???

最后引用人月神话中的观点no silver bullet,在技术选型时需要具体情况具体分析,不过鉴于GraphQL的灵活性,把它与RPC和REST配置使用,也是不错的选择。

查看原文

lxtmx 赞了文章 · 2019-01-21

RPC vs REST vs GraphQL

写在前面

最近2周的时间由于工作不忙,一直在看有关GraphQL的东西,前后端均有涉及,由于我之前做过后端开发,当时实现的接口的大体是符合RPC风格的接口。后来转做了前端开发,从实现接口者变成了调用接口者,接触最多的当属REST风格的接口。因此在这段学习GraphQL的过程中,并且也尝试使用它以全栈的角度做了一个小项目,在这个过程中,一直在思考它对比前两者在API设计的整体架构体系中的各个指标上,孰优孰劣。

其实在使用和学习的过程中,有很多文章都对比过它们的异同,但是大部分文章并没有从一个相对客观的角度来对比,更多是为了突显一个的优点而刻意指出另外一个的缺点。这让我想到一句话,脱离业务情景谈技术就是耍流氓。

昨天订阅的GraphQL Weekly中推送的一个视频正好是讲关于它们这三者的,于是就点进去看了看,发现质量还是不错的,于是就想整理出来,分享给大家。

原视频地址(油管地址,自备梯子):这里

如果没有梯子的话直接看我整理的东西也可以,我觉的应该都覆盖到视频中所讲的重点内容了。

当然,这些内容如果分开来讲,每一块内容所涉及的东西都够写一本书了,这里仅仅是简单归纳和整理,从宏观的角度来对比它们的异同,从而能够在日后面临技术选型时,有一个更佳明确的决策方向。

RPC

先简单介绍下RPC,它是Remote Procedure Call(远程过程调用)的简称。一般基于RPC协议所设计的接口,是基于网络采用客户端/服务端的模式完成调用接口的。

优点

  • 简单并且易于理解(面向开发者)
  • 轻量级的数据载体
  • 高性能

缺点

  • 对于系统本身耦合性高
  • 因为RPC本身很简单、轻量,因此很容易造成 function explosion

关于RPC的优点其实很好理解,就是因为它性能高同时又很简单,但是我认为这是对于接口提供者来讲的(因为它的高耦合性)。

但是如果从接口调用者的角度来看,高耦合性就变成了缺点,因为高耦合意味着调用者必须要足够了解系统本身的实现才能够完成调用,比如:

  • 调用者需要知道所调用接口的函数名、参数格式、参数顺序、参数名称等等
  • 如果接口提供者(server)要对接口做出一些改变,很容易对接口调用者(client)造成breaking change(违背开闭原则)
  • 一般RPC所暴露接口仅仅会暴露函数的名称和参数等信息,对于函数之间的调用关系无法提供,这意味着调用者必须足够了解系统,从能够知道如何正确的调用这些接口,但是对于接口调用者往往不需要了解过多系统内部实现细节

关于上面的第二点,为了减少breaking change,我之前实现接口的时候一般都会引入版本的概念,就是在暴露接口的方法名中加入版本号,一开始效果确实不错,但是随后就不知不觉的形成了function explosion,和视频中主讲人所举例的例子差不多,贴一下视频中的截图感受一波:

图片描述

REST

当前REST风格的API架构方式已经成了主流解决方案了,相比较RPC,它的主要不同之处在于,它是对于资源(Resource)的模型化而非步骤(Procedure)。

优点

  • 对于系统本身耦合性低,调用者不再需要了解接口内部处理和实现细节
  • 重复使用了一些 http 协议中的已定义好的部分状态动词,增强语义表现力
  • API可以随着时间而不断演进

缺点

  • 缺少约束,缺少简单、统一的规范
  • 有时候 payload 会变的冗余(overload),有时候调用api会比较繁琐(chattiness)
  • 有时候需要发送多条请求已获取数据,在网络带宽较低的场景,往往会造成不好的影响

REST的优点基本解决了RPC中存在的问题,就是解耦,从而使得前后端分离成为可能。接口提供者在修改接口时,不容易造成breaking-change,接口调用者在调用接口时,往往面向数据模型编程,而省去了了解接口本身的时间成本。

但是,我认为REST当前最大的问题在于虽然它利用http的动词约束了接口的暴露方式,同时增强了语义,但是却没有约束接口如何返回数据的最佳实践,总让人感觉只要是返回json格式的接口都可以称作REST。

我在实际工作中,经常会遇到第二条缺点所指出的问题,就是接口返回的数据冗余度很高,但是却缺少我真正需要的数据,因此不得已只能调用其他接口或者直接和后端商议修改接口,并且这种问题会在web端和移动端共用一套接口中被放大。

当前比较好的解决方案就是规范化返回数据的格式,比如json-schema或者自己制定的规范。

GraphQL

GraphQL是近来比较热门的一个技术话题,相比REST和RPC,它汲取了两者的优点,即不面向资源,也不面向过程,而是面向数据查询(ask for exactly what you want)。

同时GraphQL本身需要使用强类型的Schema来对数据模型进行定义,因此相比REST它的约束性更强。

优点

  • 网络开销低,可以在单一请求中获取REST中使用多条请求获取的资源
  • 强类型Schema(约束意味着可以根据规范形成文档、IDE、错误提示等生态工具)
  • 特别适合状数据结构的业务场景(比如好友、流程、组织架构等系统)

缺点

  • 本身的语法相比较REST和RPC均复杂一些
  • 实现方面需要配套 Caching 以解决性能瓶颈
  • 对于 API 的版本控制当前没有完善解决方案(社区的建议是不要使API版本化)
  • 仍然是新鲜事物,很多技术细节仍然处于待验证状态

鉴于GraphQL这两个星期我也仅仅是做了一些简单地使用和了解,仅仅说一下感受。

首先值得肯定的是,在某些程度上确实解决了REST的缺点所带来的问题,同时配套社区建议的各种工具和库,相比使用REST风格,全栈开发体验上升一个台阶。

但是这个看起来很好的东西为什么没有火起来呢?我觉的最主要的原因是因为GraphQL所带来的好处,大部分是对于接口调用者而言的,但是实现这部分的工作却需要接口提供者来完成。

同时GraphQL的最佳实践场景应当是类似像Facebook这样的网站,业务逻辑模型是图状数据结构,比如社交。如果在一些业务逻辑模型相对简单的场景,使用GraphQL确实不如使用REST来得简单明了、直截了当。

另外一方面是GraphQL的使用场景相当灵活,在我自己的调研项目中,我是把它当做一个类似ORM的框架来使用的,在别人的一些文章中,会把它当做一个中间层来做渐进式开发和系统升级。这应当算是另外一个优点。

到底用哪个

下面根据要设计的API类型给予一些技术选型建议。

如果是Management API,这类API的特点如下:

  • 关注于对象与资源
  • 会有多种不同的客户端
  • 需要良好的可发现性和文档

这种情景使用REST + JSON API可能会更好。

如果是Command or Action API,这类API的特点如下:

  • 面向动作或者指令
  • 仅需要简单的交互

这种情况使用RPC就足够了。

如果是Internal Micro Services API,这类API的特点如下:

  • 消息密集型
  • 对系统性能有较高要求

这种情景仍然建议使用RPC

如果是Micro Services API,这类API的特点如下:

  • 消息密集型
  • 期望系统开销较低

这种情景使用RPC或者REST均可。

如果是Data or Mobile API,这类API的特点是:

  • 数据类型是具有图状的特点
  • 希望对于高延迟场景可以有更好的优化

这种场景无疑GraphQL是最好的选择。

写在最后

提供一张表格来总览它们之间在不同指标下的表现:

耦合性约束性复杂度缓存可发现性版本控制
RPC(Function)highmediumlowcustombadhard
REST(Resource)lowlowlowhttpgoodeasy
GraphQL(Query)mediumhighmediumcustomgood???

最后引用人月神话中的观点no silver bullet,在技术选型时需要具体情况具体分析,不过鉴于GraphQL的灵活性,把它与RPC和REST配置使用,也是不错的选择。

查看原文

赞 91 收藏 62 评论 12

lxtmx 赞了文章 · 2018-07-18

打破砂锅问到底:详解Webpack中的sourcemap

关于webpack中sourcemap的文章很多,但感觉大部分是翻译官方文档的说明, 缺乏直观的用例,写这篇博客的目的是帮自己厘清这个概念, 也顺便将我自己收集的这方面的干货放在这。本文将尝试先讲清楚webpack中的sourcemap配置项的概念。

Webpack中sourcemap的配置

sourcemap是为了解决开发代码与实际运行代码不一致时帮助我们debug到原始开发代码的技术。尤其是如今前端开发中大部分的代码都经过编译,打包等工程化转换。比如开发环境下用scss写样式, 想在浏览器中在线编辑css那样编辑scss就不是那么容易了。从我自己看过的资料中, sourcemap的概念最早出现在12年, jquery1.9是较早支持sourcemap的库。这篇博客比较有代表性:Introduction to JavaScript Source Maps,阮一峰的文章JavaScript Source Map 详解也大量参考该博客。关于sourcemap的原理及作用,基本在这两篇文章中讲清楚了。回到webpack中的sourcemap,就我这几天的琢磨, 这方面资料相对比较零散,但凡搜索Webpack中sourcemap的配置, 总是能得到千篇一律的如下信息:
Sourcemap type Quality Notes

eval: 生成代码 每个模块都被eval执行,并且存在@sourceURL

cheap-eval-source-map: 转换代码(行内) 每个模块被eval执行,并且sourcemap作为eval的一个dataurl

cheap-module-eval-source-map: 原始代码(只有行内) 同样道理,但是更高的质量和更低的性能

eval-source-map: 原始代码 同样道理,但是最高的质量和最低的性能

cheap-source-map: 转换代码(行内) 生成的sourcemap没有列映射,从loaders生成的sourcemap没有被使用

cheap-module-source-map: 原始代码(只有行内) 与上面一样除了每行特点的从loader中进行映射

source-map: 原始代码 最好的sourcemap质量有完整的结果,但是会很慢

webpack中devtool的配置的官方文档在这 :webpack-devtool

疑问

反正我看完这些说明是云里雾里, 就我自己而言, 有3个疑问:

  1. eval和sourcemap有什么关系,eval模式是sourcemap吗?
  2. 包含cheap关键字的配置中只有行内是什么意思?
  3. 这几种不同的配置有什么区别?

解答

看似配置项很多, 其实只是五个关键字evalsource-mapcheapmoduleinline的任意组合。这五个关键字每一项都代表一个特性, 这四种特性可以任意组合。它们分别代表以下五种特性(单独看特性说明有点不明所以,别急,往下看):

  • eval: 使用eval包裹模块代码
  • source-map: 产生.map文件
  • cheap: 不包含列信息(关于列信息的解释下面会有详细介绍)也不包含loader的sourcemap
  • module: 包含loader的sourcemap(比如jsx to js ,babel的sourcemap)
  • inline: 将.map作为DataURI嵌入,不单独生成.map文件(这个配置项比较少见)

了解了以上各种不同特性, 再来逐一解答以上问题。

eval和sourcemap有什么关系,eval模式是sourcemap吗?

evalsource-map都是webpack中devtool的配置选项, eval模式是使用eval将webpack中每个模块包裹,然后在模块末尾添加模块来源//# souceURL, 依靠souceURL找到原始代码的位置。包含eval关键字的配置项并不单独产生.map文件(eval模式有点特殊, 它和其他模式不一样的地方是它依靠sourceURL来定位原始代码, 而其他所有选项都使用.map文件的方式来定位)。包含source-map关键字的配置项都会产生一个.map文件,该文件保存有原始代码与运行代码的映射关系, 浏览器可以通过它找到原始代码的位置。(注:包含inline关键字的配置项也会产生.map文件,但是这个map文件是经过base64编码作为DataURI嵌入),举个栗子:eval-source-mapevalsource-map的组合,可知使用eavl语句包括模块,也产生了.map文件。webpack将.map文件作为DataURI替换eval模式中末尾的//# souceURL。按照我自己的理解, eval.map文件都是sourcemap实现的不同方式,虽然大部分sourcemap的实现是通过产生.map文件, 但并不表示只能通过.map文件实现。下面是eval模式后产生的模块代码:
图片描述

包含cheap关键字的配置中只有行内是什么意思?

这里的列信息指的是代码的不包含原始代码的列信息。 官方文档对于包含cheap的解释是这样的:

> cheap-source-map - A SourceMap without **column-mappings**. SourceMaps
> from loaders are not used.

  

这句话翻译过来就是“在cheap-source-map模式下sourcemap不包含列信息,也不包含loaders的sourcemap”这里的“column-mappings”就是代码列数的意思,是否包含loaders的sourcemap有什么区别将在之后提到。debug的时候大部分人都只在意代码的行数, 很少关注列数, 列数就是该行代码从第一个字符开始到定位字符的位置(包括空白字符)包含cheap关键字的模式不包含列信息,体现在webpack中就是:如果包含cheap关键字,则产生的.map文件不包含列信息。也就是说当你在浏览器中点击该代码的位置时, 光标只定位到行数,不定位到具体字符位置。而不包含cheap关键字时, 点击控制台log将会定位到字符位置。

包含列信息后点击原始代码的定位,注意光标位置:
图片描述

不包含列信息的光标位置:
图片描述

这篇博客:Go to a line number at a specific column直观地展示了列数的概念。如果深入到webpack中的细节中体会该配置项,可以看这篇博客:SurviveJS:Source Maps ,该文章对比了webpack中所有配置项中.map文件的代码,这里截取eval-source-mapcheap-source-map的模式产生的.map文件代码中的mappings字段对比:

devtool: 'eval-source-map'

"mappings": "AAAAA,QAAQC,GAAR,CAAY,aAAZ",

devtool: 'cheap-source-map'


```
"mappings": "AAAA",
```

注:这里使用了VLQ编码,(关于VLQ编码还可参考这里:前端构建:Source Maps详解) 在VLQ编码中,逗号,表示字符列分割,分号;表示行分割。包含cheap关键字的配置项不包含列信息,也就没有逗号。关于VLQ编码, 本文最初的阮一峰的文章中有所解释。而不包含loader的sourcemap指的是不包含loader的sourcemap,不包含它时候如果你使用了诸如babel等代码编译工具时, 定位到的原始代码将是经过编译后的代码位置,而非原始代码。

比如当我用babel编译JS的时候,如果包含不包含loaders的sourcemap,此时debug到的将是编译后的代码, 而非原始代码,如图(这是使用cheap-source-map模式未包含loaders的sourcemap情况下的截图, debug的位置与之前的对比截图是同一个地方):
图片描述

这几种不同的配置有什么区别?

通过以上两个问题的解释, webpack中的sourcemap各个配置项异同应该有了一定认识,乍看之下各个配置项很难记忆, 但其实从每个关键字所代表的特性入手, 就能体会到他们的异同。他们在webpack中的主要区别一个体现在重构的性能上, 总的来说eval性能最好,source-map性能最低,但就我自身的实践来看大多用的是最完整的source-map,该模式对于不管是js还是css,scss等都能很好的覆盖, 相反其他模式都不完整, 在开发环境下重构性能似乎比不上功能的完善。
另外需要补充的是module关键字, 当加上module关键字webpack将会添加loader的sourcemap。

这篇博客:Webpack devtool source map 对于各个sourcemap配置项都作了对比和梳理, 有趣的是,作者在该文中也指出对于很多官方文档的不解,比如对于所谓的without column-mappings作者就不知道在讲什么:

A SourceMap without column-mappings. SourceMaps from loaders are not
used.No idea what that means
查看原文

赞 107 收藏 84 评论 5

lxtmx 发布了文章 · 2018-07-11

webpack4.x配置指南

简介

鉴于webpack更新太快,总结下基础配置方法,理解有限,仅做抛砖引玉之用。

起步

初始化配置文件 package.json并安装webpack

mkdir webpack-demo && cd webpack-demo
npm init -y    //-y 初始化选项默认为 yes      
npm i webpack webpack-cli -D // -D 即 -save-dev    版本4.x以上需要安装webpack-cli

创建以下目录结构、文件和内容:

webpack-demo
  |- package.json
+ |- /src
+   |- index.js
//index.js
document.write("Hello webpack4!");

创建webpack配置文件

编写开发环境和生产环境彼此独立的webpack配置文件
先添加三个文件

 webpack-demo
  |- package.json
+ |- webpack.common.js
+ |- webpack.dev.js
+ |- webpack.prod.js
  |- /src
    |- index.js
  |- /node_modules

1.webpack.common.js

用到两个基本的插件

npm i clean-webpack-plugin html-webpack-plugin -D

clean-webpack-plugin:打包时自动清除输出文件夹中未用到的文件;
html-webpack-plugin:打包时会自动生成index.html并替换已有的index.html,bundle.js也会自行添加到 html 中。

  //webpack.common.js
    const path = require('path');
    const { CleanWebpackPlugin } = require('clean-webpack-plugin');  
    const HtmlWebpackPlugin = require('html-webpack-plugin');    
    
    module.exports = {
      entry: './src/index.js',
      plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
          title: 'index'
        })
      ],
      output: {
        filename: 'bundle.js',           
        path: path.resolve(__dirname, 'dist')    //定义输出文件夹dist路径
      }
    };

2.webpack.dev.js

先安装webpack-merge,用以合并通用配置文件与开发环境配置文件

npm i webpack-merge -D

安装开发服务器devServer,作用是修改代码后实时重新加载(刷新浏览器)

npm i webpack-dev-server -D
//webpack.dev.js
  const merge = require('webpack-merge');
  const common = require('./webpack.common.js');
  const webpack = require('webpack');
    
  module.exports = merge(common,{
     devServer: {                //启用开发服务器
       contentBase: './dist',   //告诉服务器从哪提供内容,只有在想要提供静态文件时才需要
       compress: true,          //一切服务都启用gzip 压缩
       host: '0.0.0.0',         //指定使用一个host,可用ip地址访问,没有的话如果别人访问会被禁止。默认localhost。
       port: '9999',            //指定端口号,如省略,默认为”8080“
       hot: true,               //启用模块热替换特性
       inline: true,            //启用内联模式,一段处理实时重载的脚本被插入到bundle中,并且构建消息会出现在浏览器控制台
       historyApiFallback: true,//开发单页应用时有用,依赖于HTML5 history API,设为true时所有跳转将指向index.html
     },
     plugins: [
         new webpack.HotModuleReplacementPlugin(),  //webpack内置的热更新插件
     ],
     mode: 'development'
  });

devServer的更多可选参数-https://www.webpackjs.com/con...

HotModuleReplacementPlugin 模块热替换(Hot Module Replacement)插件,用以在运行时更新发生改变的模块,从而无需进行完全刷新。

3.webpack.prod.js

同样用'webpack-merge'合并通用配置文件与生产环境配置文件

//webpack.prod.js
  const merge = require('webpack-merge');
  const common = require('./webpack.common.js');
    
  module.exports = merge(common,{
    mode: "production"
  });
关于mode

此时你可能会注意到配置文件中有个mode项,webpack4中新加,作用如下:

  • --mode production 生产环境

    不需要像旧版本一样定义node环境变量
    new webpack.DefinePlugin({"process.env.NODE_ENV":JSON.stringify("production") })

    ps:许多 library 将通过与 process.env.NODE_ENV 环境变量关联,以决定 library 中应该引用哪些内容。当使用 process.env.NODE_ENV === 'production' 时,一些 library 可能针对具体用户的环境进行代码优化,从而删除或添加一些重要代码。
    

    自动开启一些插件,如:
    uglifyjs-webpack-plugin js代码压缩(所以无需再单独使用)
    NoEmitOnErrorsPlugin 编译出错时跳过输出,以确保输出资源不包含错误
    ModuleConcatenationPlugin webpack3 添加的作用域提升(Scope Hoisting)

  • --mode development 开发环境

    自行定义node环境变量为development
    new webpack.DefinePlugin({"process.env.NODE_ENV":JSON.stringify("development") })
    使用 eval 构建 module, 提升增量构建速度
    自动开启一些插件,如
    NamedModulesPlugin 使用模块热替换(HMR)时会显示模块的相对路径

具体描述:

OptionDescription
developmentSets process.env.NODE_ENV to value development. Enables NamedChunksPlugin and NamedModulesPlugin.
productionSets process.env.NODE_ENV to value production. Enables FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin and UglifyJsPlugin.
noneOpts out of any default optimization options

启动

package.json "scripts" 中添加npm脚本,从而快捷运行开发服务器 | 打包生产环境代码

//package.json
{
  "name": "webpack-demo",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack-dev-server --open --config webpack.dev.js",
    "build": "webpack --config webpack.prod.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "clean-webpack-plugin": "^0.1.19",
    "html-webpack-plugin": "^3.2.0",
    "webpack": "^4.15.1",
    "webpack-cli": "^3.0.8",
    "webpack-dev-server": "^3.1.4",
    "webpack-merge": "^4.1.3"
  }
}

"start": "webpack-dev-server --open --config webpack.dev.js",

webpack-dev-server    启动开发服务器   
--open    打开浏览器
--config webpack.dev.js    设置运行此脚本时执行的配置文件为webpack.dev.js

"build": "webpack --config webpack.prod.js"

webpack 启动webpack
--config webpack.prod.js   设置运行此脚本时执行的配置文件为webpack.prod.js
执行 npm start

此时应该可以看到 Hello webpack4!

执行 npm run build

项目文件夹中自动生成打包后的文件目录(输出文件夹dist)

 webpack-demo
  |- package.json
  |- webpack.common.js
  |- webpack.dev.js
  |- webpack.prod.js
  |- /src
    |- index.js
  |- /dist 
      | - index.html
      | - app.bundle.js
  |- /node_modules

使用sourcemap

sourcemap 能实现打包后的运行代码与源代码的映射,帮助我们debug到原始开发代码。

///webpack.dev.js
  module.exports = merge(common,{
    devtool: 'cheap-module-eval-source-map',
    ...
  });

大多数时候开发环境用'cheap-module-eval-source-map'是最好的选择,想要完整的功能又不介意构建速度的话就直接用'source-map'。具体的配置项很多,可以是eval,source-map,cheap,module,inline的任意组合。
具体每个参数的作用请查阅官方api:https://webpack.js.org/config...
也可参考这篇文章https://segmentfault.com/a/11... 这里不做详述。

代码分离

把代码分离到不同的 bundle 中,可以按需加载或并行加载这些文件。可用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。

三种常用的代码分离方法:

1.入口起点:使用 entry 配置手动地分离代码。

先在src文件夹添加一个文件another-module.js

//another-module.js
  import _ from 'lodash';
  console.log(
    _.join(['Another', 'module', 'loaded!'], ' ')
  ); 

修改index.js

//index.js
  import _ from 'lodash';
  console.log(
    _.join(['index', 'module', 'loaded!'], ' ')
  );

用到了lodash,安装下依赖

npm i lodash -S

修改webpack.common.js中entry和output配置

//webpack.common.js
  module.exports = {
    entry: {
      index: './src/index.js',
      another: './src/another-module.js'
    },
    output: {
      filename: '[name].bundle.js',            //根据入口文件名来定义输出文件名
      path: path.resolve(__dirname, 'dist')
    }
};
执行 npm run build

将生成如下构建结果:

Hash: 66f57fffc46778f3b145
Version: webpack 4.16.0
Time: 2966ms
            Asset       Size  Chunks             Chunk Names
another.bundle.js   70.4 KiB       0  [emitted]  another
  index.bundle.js   70.4 KiB       1  [emitted]  index
       index.html  251 bytes          [emitted]  
[1] (webpack)/buildin/module.js 497 bytes {0} {1} [built]
[2] (webpack)/buildin/global.js 489 bytes {0} {1} [built]
[3] ./src/another-module.js 86 bytes {0} [built]
[4] ./src/index.js 83 bytes {1} [built]
    + 1 hidden module
Child html-webpack-plugin for "index.html":
     1 asset
    [0] (webpack)/buildin/module.js 497 bytes {0} [built]
    [1] (webpack)/buildin/global.js 489 bytes {0} [built]
        + 2 hidden modules

存在的问题:

  • 如果入口 chunks 之间包含重复的模块,那些重复模块都会被引入到各个 bundle 中。
  • 不够灵活,不能将核心应用程序逻辑动态地拆分代码。

以上两点中,第一点对我们的示例来说无疑是个问题,index.js 和another-module.js中都引入了 lodash,这样就在两个 bundle 中造成重复引用。接着,我们通过使用 SplitChunks 来移除重复的模块。

2.防止重复:使用SplitChunks 去重和分离 chunk。webpack4 之前版本用的是CommonsChunkPlugin

//webpack.common.js
  const path = require('path');
  module.exports = {
    entry:  {
      index: './src/index.js',
      another: './src/another-module.js'
    },
    plugins: [
      new CleanWebpackPlugin(['dist']),
      new HtmlWebpackPlugin({
        title: 'Production'
      })
    ],
    output: {
      filename: '[name].bundle.js',            //根据入口文件名来定义输出文件名
      path: path.resolve(__dirname, 'dist')
    },
+   optimization: {
+     splitChunks: {
+       chunks: 'all'
+     }
+   }
  };
再次执行npm run build查看效果
...

vendors~another~index.bundle.js   69.5 KiB       0  [emitted]  vendors~another~index
              another.bundle.js   1.54 KiB       1  [emitted]  another
                index.bundle.js   1.54 KiB       2  [emitted]  index
                
...

观察打包后文件大小,可以看到index.bundle.jsanother.bundle.js中已经移除了重复的依赖模块。lodash 被分离到单独的vendors~another~index.bundle.jschunk中。

3.动态导入:通过模块的内联函数调用来分离代码。
略~。~

分离css

需要用到插件mini-css-extract-plugin,这个插件会将提取css到单独的文件,根据每个包含css的js文件创建一个css文件,因此,你的样式将不再内嵌到 JS bundle 中。如果你的样式文件大小较大,这会做更快提前加载,因为 CSS bundle 会跟 JS bundle 并行加载。同时还支持按需加载css和SourceMaps.

相较于旧版extract-text-webpack-plugin插件,mini-css-extract-plugin的优势有

  • 异步加载
  • 没有重复的编译
  • 更容易使用
  • Specific to CSS
  • 支持热更新
npm i mini-css-extract-plugin -D
//webpack.common.js
  const MiniCssExtractPlugin = require("mini-css-extract-plugin");
  module.exports = {
    ...
    plugins: [
      new MiniCssExtractPlugin({
        filename: "[name].css",
      })
    ],
    module: {
      rules: [
        {
          test: /\.css$/,
          use: [
            {
              loader: MiniCssExtractPlugin.loader,
              options: {
                publicPath: '../'             //可以配置输出的css文件路径
              }
            },
            "css-loader"
          ]
        }
      ]
    }
    ...
  }

注意,这个插件不兼容style-loader (用于以<style>标签形式将css-loader内部样式注入到HTML页面)。

如果想在开发环境下使用style-loader,在生产环境分离css文件,可以这么配置:

//webpack.common.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const devMode = process.env.NODE_ENV !== 'production'

module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css',
    })
  ],
  module: {
    rules: [
      {
        test: /\.(sa|sc|c)ss$/,
        use: [
          devMode ? 'style-loader' : MiniCssExtractPlugin.loader,
          'css-loader',
          {
            loader:"postcss-loader",    //本文未用到此loader...
            options: {           // 如果没有options这个选项将会报错 No PostCSS Config found
                plugins: (loader) => []
            }
          },
          'sass-loader',
        ],
      }
    ]
  }
}

很明显我们需要先安装处理样式文件的各种loader

npm i style-loader css-loader postcss-loader node-sass sass-loader -D

这里有个问题,node环境变量process.env.NODE_ENV在webpack.config中其实是undefined,之前提及的mode配置会自动定义这个环境变量,但只能在打包后的js中取到,如何在webpack的配置文件中获取这个值呢,需要引入cross-env

npm i cross-env -D

然后在package.json的脚本命令中指定环境变量

"start": "cross-env NODE_ENV=development webpack-dev-server --open --config webpack.dev.js",
"build": "cross-env NODE_ENV=production  webpack --config webpack.prod.js"

可自行添加css文件,在js中import,执行npm run build查看效果

当然也可以不获取process.env.NODE_ENV来区分环境,在dev.js和prod.js分别配置处理样式文件的rule就行了,这也是最开始我们分开写开发环境和生产环境的webpack配置文件的原因。这里提及只是方便从低版本webpack迁移到4.x。

在单个文件中提取所有CSS
配合optimization.splitChunks.cacheGroups使用

//webpack.common.js
  optimization: {
    splitChunks: {
      cacheGroups: {
        styles: {
          name: 'styles',
          test: /\.css$/,
          chunks: 'all',
          enforce: true
        }
      }
    }
  }

会额外生成一个styles.bundle.js

按照入口JS来分离css

//webpack.common.js
...

function recursiveIssuer(m) {
  if (m.issuer) {
    return recursiveIssuer(m.issuer);
  } else if (m.name) {
    return m.name;
  } else {
    return false;
  }
}

module.exports = {
  entry:  {
    index: './src/index.js',
    another: './src/another-module.js'
  },

  ...

  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {                                //分离第三方库
          test: /[\\/]node_modules[\\/]/,
          name: 'lodash',
          chunks: 'all'
        },
        indexStyles: {
          name: 'index',                        
          test: (m,c,entry = 'index') => m.constructor.name === 'CssModule' && recursiveIssuer(m) === entry,
          chunks: 'all',
          enforce: true
        },
        otherStyles: {
          name: 'another',
          test: (m,c,entry = 'another') => m.constructor.name === 'CssModule' && recursiveIssuer(m) === entry,
          chunks: 'all',
          enforce: true
        }
      }
    }
  }
};

压缩css

webpack5可能会内置CSS压缩,webpack4需要使用像optimize-css-assets-webpack-plugin这样的插件。有个问题是设置optimization.minimizer后,会覆盖上文提到的mode配置项提供的默认值,因此需要同时使用JS压缩插件UglifyJsPlugin

npm i optimize-css-assets-webpack-plugin uglifyjs-webpack-plugin -D
//webpack.prod.js
const merge = require('webpack-merge');
const common = require('./webpack.config.js');

const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");

module.exports = merge(common,{
    mode: "production",
    optimization: {
        minimizer: [
          new UglifyJsPlugin({
            cache: true,
            parallel: true,
            sourceMap: true // set to true if you want JS source maps
          }),
          new OptimizeCSSAssetsPlugin({})
        ],
    }
});

缓存

我们都知道浏览器获取资源是比较耗费时间的,所以它会使用一种名为 缓存 的技术。通过命中缓存,以降低网络流量,使网站加载速度更快。如果我们在部署新版本时不更改资源的文件名,浏览器就可能会认为它没有被更新,就会使用它的缓存版本。因此确保 webpack 编译生成的文件能够被客户端缓存,而在文件内容变化后,能够请求到新的文件是很有必要的。

通过使用 output.filename 进行文件名替换,可以确保浏览器获取到修改后的文件。[hash]替换可以用于在文件名中包含一个构建相关的hash,但是更好的方式是使用[chunkhash]替换,在文件名中包含一个 chunk相关的hash。遗憾的是chunkhash和热更新不兼容,所以开发环境和生产环境要分开配置。

//webpack.common.js
  ...
  output: {
      filename: devMode ? '[name].[hash:8].js': '[name].[chunkhash:8].js',       //数字8表示取hash标识符的前八位
      chunkFilename: devMode ? '[name].[hash:8].js': '[name].[chunkhash:8].js',  //异步模块的文件输出名
      path: path.resolve(__dirname, 'dist')
  },
  ...

关于[hash][chunkhash]的区别,简单来说,[hash]是编译(compilation)后的hash值,compilation对象代表某个版本的资源对应的编译进程。项目中任何一个文件改动,webpack就会重新创建compilation对象,然后计算新的compilation的hash值,所有的编译输出文件名都会使用相同的hash指纹,改一个就一起变。而[chunkhash]是根据具体模块文件的内容计算所得的hash值,某个文件的改动只会影响它本身的hash指纹,不会影响其他文件。

上文代码分离一节中已经提到了如何将第三方库(比如lodash或react)提取到单独的vendor chunk文件中,因为它们很少像本地的源代码那样频繁修改。利用客户端的长效缓存机制,可以消除请求,减少向服务器获取资源,同时还能保证客户端代码和服务器端代码版本一致。
除了第三方库,webpack在入口模块中,包含了某些样板(boilerplate),确切来说就是runtimemanifest。即webpack运行时的引导代码,这部分代码我们也将它单独提取出来。

//webpack.common.js
  ...
  optimization: {
    runtimeChunk: 'single',        //分离webpack运行时的引导代码
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    }
  }
  ...

我们删掉another-module.js,修改index.js如下

///index.js
import _ from 'lodash';

function component() {
    var element = document.createElement('div');

    // Lodash, now imported by this script
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');

    return element;
}

document.body.appendChild(component());
执行npm run build

产生以下输出:

Hash: 131e8681e4403392cb5d
Version: webpack 4.16.1
Time: 744ms
              Asset       Size  Chunks             Chunk Names
  index.5bc56cae.js  260 bytes       0  [emitted]  index
vendors.5d8f5a63.js   69.5 KiB       1  [emitted]  vendors
runtime.eb6eb2fb.js   1.42 KiB       2  [emitted]  runtime
         index.html  316 bytes          [emitted]  
[1] ./src/index.js 253 bytes {0} [built]
[2] (webpack)/buildin/global.js 489 bytes {1} [built]
[3] (webpack)/buildin/module.js 497 bytes {1} [built]
    + 1 hidden module
Child html-webpack-plugin for "index.html":
     1 asset
    [2] (webpack)/buildin/global.js 489 bytes {0} [built]
    [3] (webpack)/buildin/module.js 497 bytes {0} [built]
        + 2 hidden modules

可以看到编译出的文件名后加上了hash,运行时的引导代码也被单独提取出来了。

接着添加一个print.js

///print.js
export default function print(text) {
   console.log(text);
};

修改index.js

///index.js
  import _ from 'lodash';
+ import Print from './print';

  function component() {
    var element = document.createElement('div');

    // Lodash, now imported by this script
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+   element.onclick = Print.bind(null, 'Hello webpack!');

    return element;
  }

  document.body.appendChild(component());
再执行npm run build

构建结果如下:

Hash: a710a54674ea8d4b3263
Version: webpack 4.16.1
Time: 3328ms
              Asset       Size  Chunks             Chunk Names
  index.15466585.js  327 bytes       0  [emitted]  index
vendors.7bde7828.js   69.5 KiB       1  [emitted]  vendors
runtime.eb6eb2fb.js   1.42 KiB       2  [emitted]  runtime
         index.html  316 bytes          [emitted]  
[1] (webpack)/buildin/global.js 489 bytes {1} [built]
[2] (webpack)/buildin/module.js 497 bytes {1} [built]
[3] ./src/index.js + 1 modules 406 bytes {0} [built]
    | ./src/index.js 337 bytes [built]
    | ./src/print.js 64 bytes [built]
    + 1 hidden module
Child html-webpack-plugin for "index.html":
     1 asset
    [2] (webpack)/buildin/global.js 489 bytes {0} [built]
    [3] (webpack)/buildin/module.js 497 bytes {0} [built]
        + 2 hidden modules

我们期望的是,只有 index bundle 的 hash 发生变化,然而vendors也跟着变了。这是因为每个 module.id 会基于默认的解析顺序(resolve order)进行增量。也就是说,当解析顺序发生变化,ID 也会随之改变。(官方文档里说runtime的hash也发生了变化,这里并未出现)
可以使用两个插件来解决这个问题。一是NamedModulesPlugin,将使用模块的路径而不是数字标识符。此插件有助于在开发过程中输出结果的可读性,但执行时间会长一些。二是使用webpack内置插件HashedModuleIdsPlugin,推荐用于生产环境构建:

  const webpack = require('webpack');
  ...
  module.exports = {
    ...
    plugins: [
      ...
      new webpack.HashedModuleIdsPlugin(),
      ...
    ],
    ...
  };

接下来,我们可以随意修改index.js的代码或者增删print.js,再进行构建查看hash的变化。

关于css缓存
假如index.js引用了一个index.css文件,它们会共用相同的chunkhash值。这时如果index.js更改了代码,index.css文件就算内容没有任何改变也会重复构建。
我们可以使用MiniCssExtractPlugin里的contenthash,保证css文件所处的模块里只要css文件内容不变,其本身就不会重复构建。

new MiniCssExtractPlugin({
  filename: "[name].[contenthash:8].css",
  chunkFilename: "[name].[contenthash:8].css"
}),

这样基本就完成了webpack的缓存配置。

有个小问题是,当修改index.css文件代码,重新构建后index.js的hash值也一起改变了。。。
尝试了下安装插件WebpackMd5Hash可以解决

npm i webpack-md5-hash -D

but,这个插件可能会引发别的bug,好吧这里先不用,后续补充,有兴趣可自行搜索

Babel

完成了基本的配置文件编写与代码分离,开发中需要用babel将旧的浏览器或环境中的es 2015+代码转换为es5。
需要安装一些依赖。

babel-core           //必备的核心库
babel-loader         //webpack loader配置必备
babel-preset-env     //支持es2015、2016、2017,
babel-preset-stage-0 //默认向后支持 stage-1,stage-2,stage-3,
babel-runtime        
babel-plugin-transform-runtime //转译新的API
npm i babel-runtime -S
npm i babel-core babel-loader babel-preset-env babel-preset-stage-0 babel-plugin-transform-runtime  -D

创建.babelrc文件

///.babelrc
{
  "presets": [
    "env",
    "stage-0"
  ],
  "plugins": [
    ["transform-runtime", {
      "helpers": false, //建议为false
      "polyfill": false, //是否切换新的内置插件(Promise,Set,Map等)为使用非全局污染的 polyfill,根据你的网站兼容性情况来看,开启会增加很多额外的代码
      "regenerator": true //是否切换 generator 函数为不污染全局作用域的 regenerator runtime。
    }],
  ]
}

关于 babel-polyfill 与 babel-plugin-transform-runtime

babel 可以转译新的 JavaScript 语法,但并不会转化BOM里面不兼容的API比如Promise,Set,Symbol,Array.from,async 等。这时就需要 polyfill(软垫片) 来转化这些API

babel-polyfill会仿效一个完整的 ES2015+ 环境,这样你就可以使用新的内置对象比如 Promise 或WeakMap, 静态方法比如 Array.from 或者 Object.assign, 实例方法比如 Array.prototype.includes 和生成器函数(提供 regenerator 插件)。babel-polyfill缺点是它通过改写全局prototype的方式实现,会污染全局对象所以不适合第三方库的开发,且打包后代码冗余量比较大,我们可能不需要用到所有的新API,对于现代浏览器有些也不需要polyfill。

babel-plugin-transform-runtime 依赖babel-runtime,babel编译es6到es5的过程中,babel-plugin-transform-runtime会自动polyfill es5不支持的特性,这些polyfill包就是在babel-runtime这个包里(这就是为啥babel-runtime 需要作为生产依赖引入(使用 --save))。transform-runtime优点是不会污染全局变量,多次使用只会打包一次,并且统一按需引入依赖,无重复、多余引入。缺点是例如"foobar".includes("foo")等实例方法将不起作用。

React

以react开发为例,如果是搭建新的项目,可以直接安装官方脚手架create-react-app或者使用阿里的开源ui框架 Ant Design
这里仅仅提一下如何在webpack中配置react开发环境

npm install react react-dom -S

还需要安装

babel-plugin-transform-decorators-legacy //支持修饰符语法 @connect
babel-preset-react   //解析react语法
react-hot-loader     //react热更新需要在babelrc做配置
///.babelrc
  {
  "presets": [
    "env",
    "react",
    "stage-0"
  ],
  "plugins": [
    ["transform-runtime", {
      "helpers": false, //建议为false
      "polyfill": false, //是否开始polyfill,根据网站兼容性决定是否开启
      "regenerator": true 
    }],
    "react-hot-loader/babel",     //react热更新插件
    "transform-decorators-legacy" //修饰符语法转换插件
  ]
}

如果之前webpack-dev-server配置正确,这时只要把你的根组件标记为热导出,就能启用react热更新

///index.js
  import React from 'react';
  import { hot } from 'react-hot-loader';
     
  const App = () => <div>Hello World!</div>
     
  export default hot(module)(App);

别忘了配置babel-loader

///webpack.common.js
  module: {
      rules: [{
          test: /\.jsx?$/,
          use: 'babel-loader'
      }]
  }

未完待续,容老夫喝口水先...

查看原文

赞 25 收藏 14 评论 4

lxtmx 发布了文章 · 2018-07-10

webpack4.x配置指南

简介

鉴于webpack更新太快,总结下基础配置方法,理解有限,仅做抛砖引玉之用。

起步

初始化配置文件 package.json并安装webpack

mkdir webpack-demo && cd webpack-demo
npm init -y    //-y 初始化选项默认为 yes      
npm i webpack webpack-cli -D // -D 即 -save-dev    版本4.x以上需要安装webpack-cli

创建以下目录结构、文件和内容:

webpack-demo
  |- package.json
+ |- /src
+   |- index.js
//index.js
document.write("Hello webpack4!");

创建webpack配置文件

编写开发环境和生产环境彼此独立的webpack配置文件
先添加三个文件

 webpack-demo
  |- package.json
+ |- webpack.common.js
+ |- webpack.dev.js
+ |- webpack.prod.js
  |- /src
    |- index.js
  |- /node_modules

1.webpack.common.js

用到两个基本的插件

npm i clean-webpack-plugin html-webpack-plugin -D

clean-webpack-plugin:打包时自动清除输出文件夹中未用到的文件;
html-webpack-plugin:打包时会自动生成index.html并替换已有的index.html,bundle.js也会自行添加到 html 中。

  //webpack.common.js
    const path = require('path');
    const { CleanWebpackPlugin } = require('clean-webpack-plugin');  
    const HtmlWebpackPlugin = require('html-webpack-plugin');    
    
    module.exports = {
      entry: './src/index.js',
      plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
          title: 'index'
        })
      ],
      output: {
        filename: 'bundle.js',           
        path: path.resolve(__dirname, 'dist')    //定义输出文件夹dist路径
      }
    };

2.webpack.dev.js

先安装webpack-merge,用以合并通用配置文件与开发环境配置文件

npm i webpack-merge -D

安装开发服务器devServer,作用是修改代码后实时重新加载(刷新浏览器)

npm i webpack-dev-server -D
//webpack.dev.js
  const merge = require('webpack-merge');
  const common = require('./webpack.common.js');
  const webpack = require('webpack');
    
  module.exports = merge(common,{
     devServer: {                //启用开发服务器
       contentBase: './dist',   //告诉服务器从哪提供内容,只有在想要提供静态文件时才需要
       compress: true,          //一切服务都启用gzip 压缩
       host: '0.0.0.0',         //指定使用一个host,可用ip地址访问,没有的话如果别人访问会被禁止。默认localhost。
       port: '9999',            //指定端口号,如省略,默认为”8080“
       hot: true,               //启用模块热替换特性
       inline: true,            //启用内联模式,一段处理实时重载的脚本被插入到bundle中,并且构建消息会出现在浏览器控制台
       historyApiFallback: true,//开发单页应用时有用,依赖于HTML5 history API,设为true时所有跳转将指向index.html
     },
     plugins: [
         new webpack.HotModuleReplacementPlugin(),  //webpack内置的热更新插件
     ],
     mode: 'development'
  });

devServer的更多可选参数-https://www.webpackjs.com/con...

HotModuleReplacementPlugin 模块热替换(Hot Module Replacement)插件,用以在运行时更新发生改变的模块,从而无需进行完全刷新。

3.webpack.prod.js

同样用'webpack-merge'合并通用配置文件与生产环境配置文件

//webpack.prod.js
  const merge = require('webpack-merge');
  const common = require('./webpack.common.js');
    
  module.exports = merge(common,{
    mode: "production"
  });
关于mode

此时你可能会注意到配置文件中有个mode项,webpack4中新加,作用如下:

  • --mode production 生产环境

    不需要像旧版本一样定义node环境变量
    new webpack.DefinePlugin({"process.env.NODE_ENV":JSON.stringify("production") })

    ps:许多 library 将通过与 process.env.NODE_ENV 环境变量关联,以决定 library 中应该引用哪些内容。当使用 process.env.NODE_ENV === 'production' 时,一些 library 可能针对具体用户的环境进行代码优化,从而删除或添加一些重要代码。
    

    自动开启一些插件,如:
    uglifyjs-webpack-plugin js代码压缩(所以无需再单独使用)
    NoEmitOnErrorsPlugin 编译出错时跳过输出,以确保输出资源不包含错误
    ModuleConcatenationPlugin webpack3 添加的作用域提升(Scope Hoisting)

  • --mode development 开发环境

    自行定义node环境变量为development
    new webpack.DefinePlugin({"process.env.NODE_ENV":JSON.stringify("development") })
    使用 eval 构建 module, 提升增量构建速度
    自动开启一些插件,如
    NamedModulesPlugin 使用模块热替换(HMR)时会显示模块的相对路径

具体描述:

OptionDescription
developmentSets process.env.NODE_ENV to value development. Enables NamedChunksPlugin and NamedModulesPlugin.
productionSets process.env.NODE_ENV to value production. Enables FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin and UglifyJsPlugin.
noneOpts out of any default optimization options

启动

package.json "scripts" 中添加npm脚本,从而快捷运行开发服务器 | 打包生产环境代码

//package.json
{
  "name": "webpack-demo",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack-dev-server --open --config webpack.dev.js",
    "build": "webpack --config webpack.prod.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "clean-webpack-plugin": "^0.1.19",
    "html-webpack-plugin": "^3.2.0",
    "webpack": "^4.15.1",
    "webpack-cli": "^3.0.8",
    "webpack-dev-server": "^3.1.4",
    "webpack-merge": "^4.1.3"
  }
}

"start": "webpack-dev-server --open --config webpack.dev.js",

webpack-dev-server    启动开发服务器   
--open    打开浏览器
--config webpack.dev.js    设置运行此脚本时执行的配置文件为webpack.dev.js

"build": "webpack --config webpack.prod.js"

webpack 启动webpack
--config webpack.prod.js   设置运行此脚本时执行的配置文件为webpack.prod.js
执行 npm start

此时应该可以看到 Hello webpack4!

执行 npm run build

项目文件夹中自动生成打包后的文件目录(输出文件夹dist)

 webpack-demo
  |- package.json
  |- webpack.common.js
  |- webpack.dev.js
  |- webpack.prod.js
  |- /src
    |- index.js
  |- /dist 
      | - index.html
      | - app.bundle.js
  |- /node_modules

使用sourcemap

sourcemap 能实现打包后的运行代码与源代码的映射,帮助我们debug到原始开发代码。

///webpack.dev.js
  module.exports = merge(common,{
    devtool: 'cheap-module-eval-source-map',
    ...
  });

大多数时候开发环境用'cheap-module-eval-source-map'是最好的选择,想要完整的功能又不介意构建速度的话就直接用'source-map'。具体的配置项很多,可以是eval,source-map,cheap,module,inline的任意组合。
具体每个参数的作用请查阅官方api:https://webpack.js.org/config...
也可参考这篇文章https://segmentfault.com/a/11... 这里不做详述。

代码分离

把代码分离到不同的 bundle 中,可以按需加载或并行加载这些文件。可用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。

三种常用的代码分离方法:

1.入口起点:使用 entry 配置手动地分离代码。

先在src文件夹添加一个文件another-module.js

//another-module.js
  import _ from 'lodash';
  console.log(
    _.join(['Another', 'module', 'loaded!'], ' ')
  ); 

修改index.js

//index.js
  import _ from 'lodash';
  console.log(
    _.join(['index', 'module', 'loaded!'], ' ')
  );

用到了lodash,安装下依赖

npm i lodash -S

修改webpack.common.js中entry和output配置

//webpack.common.js
  module.exports = {
    entry: {
      index: './src/index.js',
      another: './src/another-module.js'
    },
    output: {
      filename: '[name].bundle.js',            //根据入口文件名来定义输出文件名
      path: path.resolve(__dirname, 'dist')
    }
};
执行 npm run build

将生成如下构建结果:

Hash: 66f57fffc46778f3b145
Version: webpack 4.16.0
Time: 2966ms
            Asset       Size  Chunks             Chunk Names
another.bundle.js   70.4 KiB       0  [emitted]  another
  index.bundle.js   70.4 KiB       1  [emitted]  index
       index.html  251 bytes          [emitted]  
[1] (webpack)/buildin/module.js 497 bytes {0} {1} [built]
[2] (webpack)/buildin/global.js 489 bytes {0} {1} [built]
[3] ./src/another-module.js 86 bytes {0} [built]
[4] ./src/index.js 83 bytes {1} [built]
    + 1 hidden module
Child html-webpack-plugin for "index.html":
     1 asset
    [0] (webpack)/buildin/module.js 497 bytes {0} [built]
    [1] (webpack)/buildin/global.js 489 bytes {0} [built]
        + 2 hidden modules

存在的问题:

  • 如果入口 chunks 之间包含重复的模块,那些重复模块都会被引入到各个 bundle 中。
  • 不够灵活,不能将核心应用程序逻辑动态地拆分代码。

以上两点中,第一点对我们的示例来说无疑是个问题,index.js 和another-module.js中都引入了 lodash,这样就在两个 bundle 中造成重复引用。接着,我们通过使用 SplitChunks 来移除重复的模块。

2.防止重复:使用SplitChunks 去重和分离 chunk。webpack4 之前版本用的是CommonsChunkPlugin

//webpack.common.js
  const path = require('path');
  module.exports = {
    entry:  {
      index: './src/index.js',
      another: './src/another-module.js'
    },
    plugins: [
      new CleanWebpackPlugin(['dist']),
      new HtmlWebpackPlugin({
        title: 'Production'
      })
    ],
    output: {
      filename: '[name].bundle.js',            //根据入口文件名来定义输出文件名
      path: path.resolve(__dirname, 'dist')
    },
+   optimization: {
+     splitChunks: {
+       chunks: 'all'
+     }
+   }
  };
再次执行npm run build查看效果
...

vendors~another~index.bundle.js   69.5 KiB       0  [emitted]  vendors~another~index
              another.bundle.js   1.54 KiB       1  [emitted]  another
                index.bundle.js   1.54 KiB       2  [emitted]  index
                
...

观察打包后文件大小,可以看到index.bundle.jsanother.bundle.js中已经移除了重复的依赖模块。lodash 被分离到单独的vendors~another~index.bundle.jschunk中。

3.动态导入:通过模块的内联函数调用来分离代码。
略~。~

分离css

需要用到插件mini-css-extract-plugin,这个插件会将提取css到单独的文件,根据每个包含css的js文件创建一个css文件,因此,你的样式将不再内嵌到 JS bundle 中。如果你的样式文件大小较大,这会做更快提前加载,因为 CSS bundle 会跟 JS bundle 并行加载。同时还支持按需加载css和SourceMaps.

相较于旧版extract-text-webpack-plugin插件,mini-css-extract-plugin的优势有

  • 异步加载
  • 没有重复的编译
  • 更容易使用
  • Specific to CSS
  • 支持热更新
npm i mini-css-extract-plugin -D
//webpack.common.js
  const MiniCssExtractPlugin = require("mini-css-extract-plugin");
  module.exports = {
    ...
    plugins: [
      new MiniCssExtractPlugin({
        filename: "[name].css",
      })
    ],
    module: {
      rules: [
        {
          test: /\.css$/,
          use: [
            {
              loader: MiniCssExtractPlugin.loader,
              options: {
                publicPath: '../'             //可以配置输出的css文件路径
              }
            },
            "css-loader"
          ]
        }
      ]
    }
    ...
  }

注意,这个插件不兼容style-loader (用于以<style>标签形式将css-loader内部样式注入到HTML页面)。

如果想在开发环境下使用style-loader,在生产环境分离css文件,可以这么配置:

//webpack.common.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const devMode = process.env.NODE_ENV !== 'production'

module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css',
    })
  ],
  module: {
    rules: [
      {
        test: /\.(sa|sc|c)ss$/,
        use: [
          devMode ? 'style-loader' : MiniCssExtractPlugin.loader,
          'css-loader',
          {
            loader:"postcss-loader",    //本文未用到此loader...
            options: {           // 如果没有options这个选项将会报错 No PostCSS Config found
                plugins: (loader) => []
            }
          },
          'sass-loader',
        ],
      }
    ]
  }
}

很明显我们需要先安装处理样式文件的各种loader

npm i style-loader css-loader postcss-loader node-sass sass-loader -D

这里有个问题,node环境变量process.env.NODE_ENV在webpack.config中其实是undefined,之前提及的mode配置会自动定义这个环境变量,但只能在打包后的js中取到,如何在webpack的配置文件中获取这个值呢,需要引入cross-env

npm i cross-env -D

然后在package.json的脚本命令中指定环境变量

"start": "cross-env NODE_ENV=development webpack-dev-server --open --config webpack.dev.js",
"build": "cross-env NODE_ENV=production  webpack --config webpack.prod.js"

可自行添加css文件,在js中import,执行npm run build查看效果

当然也可以不获取process.env.NODE_ENV来区分环境,在dev.js和prod.js分别配置处理样式文件的rule就行了,这也是最开始我们分开写开发环境和生产环境的webpack配置文件的原因。这里提及只是方便从低版本webpack迁移到4.x。

在单个文件中提取所有CSS
配合optimization.splitChunks.cacheGroups使用

//webpack.common.js
  optimization: {
    splitChunks: {
      cacheGroups: {
        styles: {
          name: 'styles',
          test: /\.css$/,
          chunks: 'all',
          enforce: true
        }
      }
    }
  }

会额外生成一个styles.bundle.js

按照入口JS来分离css

//webpack.common.js
...

function recursiveIssuer(m) {
  if (m.issuer) {
    return recursiveIssuer(m.issuer);
  } else if (m.name) {
    return m.name;
  } else {
    return false;
  }
}

module.exports = {
  entry:  {
    index: './src/index.js',
    another: './src/another-module.js'
  },

  ...

  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {                                //分离第三方库
          test: /[\\/]node_modules[\\/]/,
          name: 'lodash',
          chunks: 'all'
        },
        indexStyles: {
          name: 'index',                        
          test: (m,c,entry = 'index') => m.constructor.name === 'CssModule' && recursiveIssuer(m) === entry,
          chunks: 'all',
          enforce: true
        },
        otherStyles: {
          name: 'another',
          test: (m,c,entry = 'another') => m.constructor.name === 'CssModule' && recursiveIssuer(m) === entry,
          chunks: 'all',
          enforce: true
        }
      }
    }
  }
};

压缩css

webpack5可能会内置CSS压缩,webpack4需要使用像optimize-css-assets-webpack-plugin这样的插件。有个问题是设置optimization.minimizer后,会覆盖上文提到的mode配置项提供的默认值,因此需要同时使用JS压缩插件UglifyJsPlugin

npm i optimize-css-assets-webpack-plugin uglifyjs-webpack-plugin -D
//webpack.prod.js
const merge = require('webpack-merge');
const common = require('./webpack.config.js');

const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");

module.exports = merge(common,{
    mode: "production",
    optimization: {
        minimizer: [
          new UglifyJsPlugin({
            cache: true,
            parallel: true,
            sourceMap: true // set to true if you want JS source maps
          }),
          new OptimizeCSSAssetsPlugin({})
        ],
    }
});

缓存

我们都知道浏览器获取资源是比较耗费时间的,所以它会使用一种名为 缓存 的技术。通过命中缓存,以降低网络流量,使网站加载速度更快。如果我们在部署新版本时不更改资源的文件名,浏览器就可能会认为它没有被更新,就会使用它的缓存版本。因此确保 webpack 编译生成的文件能够被客户端缓存,而在文件内容变化后,能够请求到新的文件是很有必要的。

通过使用 output.filename 进行文件名替换,可以确保浏览器获取到修改后的文件。[hash]替换可以用于在文件名中包含一个构建相关的hash,但是更好的方式是使用[chunkhash]替换,在文件名中包含一个 chunk相关的hash。遗憾的是chunkhash和热更新不兼容,所以开发环境和生产环境要分开配置。

//webpack.common.js
  ...
  output: {
      filename: devMode ? '[name].[hash:8].js': '[name].[chunkhash:8].js',       //数字8表示取hash标识符的前八位
      chunkFilename: devMode ? '[name].[hash:8].js': '[name].[chunkhash:8].js',  //异步模块的文件输出名
      path: path.resolve(__dirname, 'dist')
  },
  ...

关于[hash][chunkhash]的区别,简单来说,[hash]是编译(compilation)后的hash值,compilation对象代表某个版本的资源对应的编译进程。项目中任何一个文件改动,webpack就会重新创建compilation对象,然后计算新的compilation的hash值,所有的编译输出文件名都会使用相同的hash指纹,改一个就一起变。而[chunkhash]是根据具体模块文件的内容计算所得的hash值,某个文件的改动只会影响它本身的hash指纹,不会影响其他文件。

上文代码分离一节中已经提到了如何将第三方库(比如lodash或react)提取到单独的vendor chunk文件中,因为它们很少像本地的源代码那样频繁修改。利用客户端的长效缓存机制,可以消除请求,减少向服务器获取资源,同时还能保证客户端代码和服务器端代码版本一致。
除了第三方库,webpack在入口模块中,包含了某些样板(boilerplate),确切来说就是runtimemanifest。即webpack运行时的引导代码,这部分代码我们也将它单独提取出来。

//webpack.common.js
  ...
  optimization: {
    runtimeChunk: 'single',        //分离webpack运行时的引导代码
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    }
  }
  ...

我们删掉another-module.js,修改index.js如下

///index.js
import _ from 'lodash';

function component() {
    var element = document.createElement('div');

    // Lodash, now imported by this script
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');

    return element;
}

document.body.appendChild(component());
执行npm run build

产生以下输出:

Hash: 131e8681e4403392cb5d
Version: webpack 4.16.1
Time: 744ms
              Asset       Size  Chunks             Chunk Names
  index.5bc56cae.js  260 bytes       0  [emitted]  index
vendors.5d8f5a63.js   69.5 KiB       1  [emitted]  vendors
runtime.eb6eb2fb.js   1.42 KiB       2  [emitted]  runtime
         index.html  316 bytes          [emitted]  
[1] ./src/index.js 253 bytes {0} [built]
[2] (webpack)/buildin/global.js 489 bytes {1} [built]
[3] (webpack)/buildin/module.js 497 bytes {1} [built]
    + 1 hidden module
Child html-webpack-plugin for "index.html":
     1 asset
    [2] (webpack)/buildin/global.js 489 bytes {0} [built]
    [3] (webpack)/buildin/module.js 497 bytes {0} [built]
        + 2 hidden modules

可以看到编译出的文件名后加上了hash,运行时的引导代码也被单独提取出来了。

接着添加一个print.js

///print.js
export default function print(text) {
   console.log(text);
};

修改index.js

///index.js
  import _ from 'lodash';
+ import Print from './print';

  function component() {
    var element = document.createElement('div');

    // Lodash, now imported by this script
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+   element.onclick = Print.bind(null, 'Hello webpack!');

    return element;
  }

  document.body.appendChild(component());
再执行npm run build

构建结果如下:

Hash: a710a54674ea8d4b3263
Version: webpack 4.16.1
Time: 3328ms
              Asset       Size  Chunks             Chunk Names
  index.15466585.js  327 bytes       0  [emitted]  index
vendors.7bde7828.js   69.5 KiB       1  [emitted]  vendors
runtime.eb6eb2fb.js   1.42 KiB       2  [emitted]  runtime
         index.html  316 bytes          [emitted]  
[1] (webpack)/buildin/global.js 489 bytes {1} [built]
[2] (webpack)/buildin/module.js 497 bytes {1} [built]
[3] ./src/index.js + 1 modules 406 bytes {0} [built]
    | ./src/index.js 337 bytes [built]
    | ./src/print.js 64 bytes [built]
    + 1 hidden module
Child html-webpack-plugin for "index.html":
     1 asset
    [2] (webpack)/buildin/global.js 489 bytes {0} [built]
    [3] (webpack)/buildin/module.js 497 bytes {0} [built]
        + 2 hidden modules

我们期望的是,只有 index bundle 的 hash 发生变化,然而vendors也跟着变了。这是因为每个 module.id 会基于默认的解析顺序(resolve order)进行增量。也就是说,当解析顺序发生变化,ID 也会随之改变。(官方文档里说runtime的hash也发生了变化,这里并未出现)
可以使用两个插件来解决这个问题。一是NamedModulesPlugin,将使用模块的路径而不是数字标识符。此插件有助于在开发过程中输出结果的可读性,但执行时间会长一些。二是使用webpack内置插件HashedModuleIdsPlugin,推荐用于生产环境构建:

  const webpack = require('webpack');
  ...
  module.exports = {
    ...
    plugins: [
      ...
      new webpack.HashedModuleIdsPlugin(),
      ...
    ],
    ...
  };

接下来,我们可以随意修改index.js的代码或者增删print.js,再进行构建查看hash的变化。

关于css缓存
假如index.js引用了一个index.css文件,它们会共用相同的chunkhash值。这时如果index.js更改了代码,index.css文件就算内容没有任何改变也会重复构建。
我们可以使用MiniCssExtractPlugin里的contenthash,保证css文件所处的模块里只要css文件内容不变,其本身就不会重复构建。

new MiniCssExtractPlugin({
  filename: "[name].[contenthash:8].css",
  chunkFilename: "[name].[contenthash:8].css"
}),

这样基本就完成了webpack的缓存配置。

有个小问题是,当修改index.css文件代码,重新构建后index.js的hash值也一起改变了。。。
尝试了下安装插件WebpackMd5Hash可以解决

npm i webpack-md5-hash -D

but,这个插件可能会引发别的bug,好吧这里先不用,后续补充,有兴趣可自行搜索

Babel

完成了基本的配置文件编写与代码分离,开发中需要用babel将旧的浏览器或环境中的es 2015+代码转换为es5。
需要安装一些依赖。

babel-core           //必备的核心库
babel-loader         //webpack loader配置必备
babel-preset-env     //支持es2015、2016、2017,
babel-preset-stage-0 //默认向后支持 stage-1,stage-2,stage-3,
babel-runtime        
babel-plugin-transform-runtime //转译新的API
npm i babel-runtime -S
npm i babel-core babel-loader babel-preset-env babel-preset-stage-0 babel-plugin-transform-runtime  -D

创建.babelrc文件

///.babelrc
{
  "presets": [
    "env",
    "stage-0"
  ],
  "plugins": [
    ["transform-runtime", {
      "helpers": false, //建议为false
      "polyfill": false, //是否切换新的内置插件(Promise,Set,Map等)为使用非全局污染的 polyfill,根据你的网站兼容性情况来看,开启会增加很多额外的代码
      "regenerator": true //是否切换 generator 函数为不污染全局作用域的 regenerator runtime。
    }],
  ]
}

关于 babel-polyfill 与 babel-plugin-transform-runtime

babel 可以转译新的 JavaScript 语法,但并不会转化BOM里面不兼容的API比如Promise,Set,Symbol,Array.from,async 等。这时就需要 polyfill(软垫片) 来转化这些API

babel-polyfill会仿效一个完整的 ES2015+ 环境,这样你就可以使用新的内置对象比如 Promise 或WeakMap, 静态方法比如 Array.from 或者 Object.assign, 实例方法比如 Array.prototype.includes 和生成器函数(提供 regenerator 插件)。babel-polyfill缺点是它通过改写全局prototype的方式实现,会污染全局对象所以不适合第三方库的开发,且打包后代码冗余量比较大,我们可能不需要用到所有的新API,对于现代浏览器有些也不需要polyfill。

babel-plugin-transform-runtime 依赖babel-runtime,babel编译es6到es5的过程中,babel-plugin-transform-runtime会自动polyfill es5不支持的特性,这些polyfill包就是在babel-runtime这个包里(这就是为啥babel-runtime 需要作为生产依赖引入(使用 --save))。transform-runtime优点是不会污染全局变量,多次使用只会打包一次,并且统一按需引入依赖,无重复、多余引入。缺点是例如"foobar".includes("foo")等实例方法将不起作用。

React

以react开发为例,如果是搭建新的项目,可以直接安装官方脚手架create-react-app或者使用阿里的开源ui框架 Ant Design
这里仅仅提一下如何在webpack中配置react开发环境

npm install react react-dom -S

还需要安装

babel-plugin-transform-decorators-legacy //支持修饰符语法 @connect
babel-preset-react   //解析react语法
react-hot-loader     //react热更新需要在babelrc做配置
///.babelrc
  {
  "presets": [
    "env",
    "react",
    "stage-0"
  ],
  "plugins": [
    ["transform-runtime", {
      "helpers": false, //建议为false
      "polyfill": false, //是否开始polyfill,根据网站兼容性决定是否开启
      "regenerator": true 
    }],
    "react-hot-loader/babel",     //react热更新插件
    "transform-decorators-legacy" //修饰符语法转换插件
  ]
}

如果之前webpack-dev-server配置正确,这时只要把你的根组件标记为热导出,就能启用react热更新

///index.js
  import React from 'react';
  import { hot } from 'react-hot-loader';
     
  const App = () => <div>Hello World!</div>
     
  export default hot(module)(App);

别忘了配置babel-loader

///webpack.common.js
  module: {
      rules: [{
          test: /\.jsx?$/,
          use: 'babel-loader'
      }]
  }

未完待续,容老夫喝口水先...

查看原文

赞 25 收藏 14 评论 4

lxtmx 关注了用户 · 2018-07-07

jansen @jansen

学习不能止步,学习就是兴趣!终生学习是目标

关注 12

lxtmx 关注了专栏 · 2017-02-13

Jansen在路上

记录前端路

关注 4

lxtmx 关注了用户 · 2017-02-13

jansen @jansen

学习不能止步,学习就是兴趣!终生学习是目标

关注 12

认证与成就

  • 获得 25 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-02-13
个人主页被 539 人浏览