从了解Webpack到搭建vue开发环境

736 阅读9分钟

前言

最近刚好利用vue-cli创建Vue项目,其中对于打包的原理还是不够了解,在此记录一下。本人水平有限,本着分享快乐的精神主旨,希望能给大家提供一些参考,如果有错误的地方希望大家不吝指正。

Webpack

为了进一步了解vue开发环境是怎么打包的,首先咱们先了解一下webpack的工作原理

初始化

这里选取 webpack@4.43.0 和 webpack-cli@3.3.12 版本

$ npm init -y
$ npm i webpack@4.43.0 webpack-cli@3.3.12 -D

测试

package.json 文件添加代码

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "webpack"
},

在项目根目录下创建 ./src/index.js 文件

console.log("Hello Webpack !!!");

执行命令

$ npm run dev

然后在根目录下就会发现多了 ./dist/index.js 文件。
这边稍微说一下为什么没有任何配置还能执行成功。因为从 webpack v4.0.0 开始,可以不用引入一个配置文件。webpack的默认配置下:entry(入口起点)指向./src目录下,output(出口)指向./dist目录下。

webpack初始化配置

项目根目录下创建webpack.config.js

const path = require("path");
module.exports = {
    entry: "./src/index.js",
    output: {
        path: path.resolve(__dirname, "dist"),
        filename: "bundle.js"
    },
    mode: "development"
}

这里为了更好的观察bundle.js的内部结构,咱们先选择webpack开发模式。然后执行命令npm run dev得到一个未压缩的bundle.js文件

bundle.js 浅析

通过以上生成的bundle.js文件,咱们先把代码内部掏空,最终得到一个自执行函数

(function(modules) {}) ({});

从上面可以看到,自执行函数传入的参数是一个对象

(function(modules) {}) ({
    "./src/index.js":
    (function(module, exports) {
        eval("console.log('Hello Webpack !!!');\n\n//# sourceURL=webpack:///./src/index.js?");
    })
});

执行第一个入口模块

(function(modules) {
    function __webpack_require__(moduleId) {...}
    return __webpack_require__(__webpack_require__.s = "./src/index.js");
}) ({
    "./src/index.js": 
    (function(module, exports) {
        eval("console.log('Hello Webpack !!!');\n\n//# sourceURL=webpack:///./src/index.js?");
    })
});

函数内部有一个__webpack_require__(moduleId)的模块加载函数,用来缓存模块和执行模块函数。

(function(modules) {
    var installedModules = {};
    function __webpack_require__(moduleId) {
        // 当其他模块引用已经缓存的模块 直接返回不做处理
        if(installedModules[moduleId]) {
                return installedModules[moduleId].exports;
        }
        
        // 创建一个新的模块并放入缓存中
        var module = installedModules[moduleId] = {
                i: moduleId,
                l: false,
                exports: {}
        };

        // 执行模块函数 
        // 使用call为了确保每个module中的this指向的是module本身
        // 传入webpack_require__函数为提供加载其他依赖module的能力
        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
        
        // 导出模块
        return module.exports;
    }
    ...
}) ({});

为了更好的了解执行模块函数是怎么操作的那么这里咱们先创建一个./src/home.js文件

console.log("It`s home.js !!!");

更新 index.js 使 home.js 成为它的依赖

import './home';
console.log('Hello Webpack !!!');

执行命令得到

(function(modules) {
    function __webpack_require__(moduleId) {
        ...
        // 执行模块函数 
        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
        ...
    }
    // 对模块加载函数做兼容性处理
    ...
    __webpack_require__.r = function(exports) {...}
    __webpack_require__.n =function(module) {...}
    ...
}) ({
    "./src/home.js":
    (function(module, exports) {
        eval("console.log(\"It`s home.js !!!\");\n\n//# sourceURL=webpack:///./src/home.js?");
    }),
    "./src/index.js":
    (function(module, __webpack_exports__, __webpack_require__) {
    "use strict";
        eval("
            __webpack_require__.r(__webpack_exports__);
            // 依赖 ./src/home.js 先处理
            var _home__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(\"./src/home.js\");
            var _home__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(_home__WEBPACK_IMPORTED_MODULE_0__);
            console.log('Hello Webpack !!!');
            ");
        })
});

从上面可以看到函数先去加载index.js然后运行index.js的代码,在执行eval函数时遇到__webpack_require__函数,表示当前模块 index.js 依赖了其他模块 home.js,然执行完依赖函数 home.js 再回到index.js处理剩下的代码。

大致流程类似Nodejs Koa框架的洋葱圈模型或者说递归函数调用

__webpack_require__(./src/index.js)
------------------------------------------------
    // index.js 
    __webpack_require__(./src/home.js)
   |------------------------------------ |
   |     // home.js                      |
   |     console.log("It`s home.js !!!");|
   |-------------------------------------|
   console.log("Hello Webpack !!!")
    
------------------------------------------------

最终输出结果是

It`s home.js !!!
Hello Webpack !!!

配置

上面只是对webpack稍作了解,然后咱们开始做一些比较基础的配置。

首先,咱们先配置一个最基础的打包功能的项目。创建生产环境和开发环境的webpack配置文件。为了更好的让项目看起来合理化,咱们先创建几个配置文件放到./build文件:

  • webpack.base.config.js 是开发环境和生产环境下共同的配置
const path = require('path');

module.exports = {
  entry: {
    bundle: path.resolve(__dirname, '../src/main.js')
  },
  output: {
    path: path.resolve(__dirname, '../dist'),
    filename: '[name].js'
  },
  module: {
    rules:[]
  },
  plugins: [
      new HtmlWebpackPlugin({
        template: path.resolve(__dirname, '../public/index.html')
      })
  ]
};
  • webpack.dev.config.js 开发环境配置
const merge = require('webpack-merge');
const path = require('path');
const baseConfig = require('./webpack.base.config');

module.exports = merge(baseConfig, {
  mode: 'development',
  devtool: "cheap-module-eval-source-map",
  devServer: {
    contentBase: path.resolve(__dirname, '../dist'),
    open: true
  }
});
  • webpack.prod.config.js 生成环境配置
const merge = require('webpack-merge');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const baseConfig = require('./webpack.base.config');

module.exports = merge(baseConfig, {
  mode: 'production',
  devtool: 'none',
  plugins: [
    new CleanWebpackPlugin()
  ]
});

注意以上配置含有webpack插件依赖

$ npm i -D webpack-merge@4 clean-webpack-plugin webpack-dev-server html-webpack-plugin@4

为了贴近vue-cli的文件结构,这里将./src/index.js修改为./src/main.js,创建./public/index.html文件

修改命令配置package.json

"scripts": {
    "dev": "webpack-dev-server --inline --progress --config ./build/webpack.dev.conf.js",
    "build": "webpack --progress --color --config ./build/webpack.prod.conf.js"
},

执行命令

$ npm run dev
$ npm run build

提示:在运行过程中如果有出错大部分原因是包版本的问题

资源处理

大家都知道webpack是一个js打包工具(意思就是只能处理javaScript代码),但是实际项目上不仅仅只有js,这时候就需要配合loaderplugin来帮忙处理非js资源。

loader 用于对模块的源代码进行转换。loader 可以使你在 import 或"加载"模块时预处理文件。因此,loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的强大方法。loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript,或将内联图像转换为 data URL。loader 甚至允许你直接在 JavaScript 模块中 import CSS文件!

这里咱们简单了解几个常见的loader:

JS处理

babel-loader

Babel 是一个 JavaScript 编译器,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。

配置需要适配的浏览器版本 package.json

"browserslist": [
    "last 2 versions",
    "> 1%",
    "not ie <= 8"
]

babel-loader 只是 webpack 与 babel 的通信桥梁,并不会去做es6转成es5的⼯作,这部分⼯作是交给@babel/preset-env来做
安装

$ npm i babel-loader @babel/core @babel/preset-env -D

在项目根目录下创建.babelrc文件

{
    "presets": [
        [
            "@babel/preset-env"
        ]
    ]
}

webpack.base.config.js文件中的module.rules数组新增对象

{
    test: /\.js$/,
    exclude: /node_modules/,
    use: {
        loader: "babel-loader"
    }
}

这里做一下测试,修改main.js文件内容

// Syntax
let a = 10;
const fn = () => {};
let mapData = ['1','2'].map(item => item);
class Test1 {}
class Test2 {}

// Api
const c = [1, 2, 3].includes(1);
new Promise(() => {});

打包后的结果是

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

// Syntax
var a = 10;

var fn = function fn() {};

var mapData = ['1', '2'].map(function (item) {
  return item;
});

var Test1 = function Test1() {
  _classCallCheck(this, Test1);
};

var Test2 = function Test2() {
  _classCallCheck(this, Test2);
}; 

// Api
var c = [1, 2, 3].includes(1);
new Promise(function () {});

从上面注释可以看到Syntax下的代码都被转化了,而Api下的代码却没有任何变化,这是为什么呢?
babel 在转译的时候,会将源代码分成 Syntax 和 Api 两部分来处理:

  • Syntax:类似于展开对象、Optional Chaining、let、const 等语法
  • Api:类似于 [1,2,3].includes 等函数、方法 babel 转译后的代码如果在不支持 includes、Promise 等Api方法的浏览器里运行,就会报错。转换includes、Promise等api方法,需要借助@babel/polyfill,把es的新特性都装进来,来弥补低版本浏览器中缺失的特性。 安装
$ npm install --save @babel/polyfill

然后再main.js顶部代码添加

import "@babel/polyfill";

添加新特性依赖后,打包

__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _babel_polyfill__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/polyfill */ "./node_modules/@babel/polyfill/lib/index.js");
/* harmony import */ var _babel_polyfill__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_babel_polyfill__WEBPACK_IMPORTED_MODULE_0__);
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

 // syntax

var a = 10;

var fn = function fn() {};

var mapData = ['1', '2'].map(function (item) {
  return item;
});

var Test1 = function Test1() {
  _classCallCheck(this, Test1);
};

var Test2 = function Test2() {
  _classCallCheck(this, Test2);
}; // api


var c = [1, 2, 3].includes(1);
new Promise(function () {});

打包后代码体积为 911 kib (development模式下)
可以看到,babel 默认会将所有的 polyfill 特性注⼊,这样会导致结果的包大小非常大,而我们这里仅仅需要 includes、Promise 两个方法而已。所以我们需要按需加载polyfill特性,从而减少打包的体积。

Since @babel/polyfill was deprecated in 7.4.0, we recommend directly adding core-js and setting the version via the corejs option.

自从babel在7.4.0版本后弃用了@babel/polyfill后,babel建议我们直接使用core-js并且设置corejs的版本来替换polyfill。
删除 index.js 中的 import "@babel/polyfill"; 并安装core-js@3依赖

$ npm install -S core-js@3

修改.bablerc

{
	"presets": [
        [
            "@babel/preset-env",
            {
                "corejs": 3, // 新版本需要指定核⼼库版本
                "useBuiltIns": "usage" //按需注⼊
            }
        ]
    ]
}

打包后

__webpack_require__.r(__webpack_exports__);
/* harmony import */ var core_js_modules_es_array_map_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! core-js/modules/es.array.map.js */ "./node_modules/core-js/modules/es.array.map.js");
/* harmony import */ var core_js_modules_es_array_map_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_array_map_js__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! core-js/modules/es.array.includes.js */ "./node_modules/core-js/modules/es.array.includes.js");
/* harmony import */ var core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_1__);
/* harmony import */ var core_js_modules_es_object_to_string_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! core-js/modules/es.object.to-string.js */ "./node_modules/core-js/modules/es.object.to-string.js");
/* harmony import */ var core_js_modules_es_object_to_string_js__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_object_to_string_js__WEBPACK_IMPORTED_MODULE_2__);
/* harmony import */ var core_js_modules_es_promise_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! core-js/modules/es.promise.js */ "./node_modules/core-js/modules/es.promise.js");
/* harmony import */ var core_js_modules_es_promise_js__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_promise_js__WEBPACK_IMPORTED_MODULE_3__);
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }

// Syntax
var a = 10;

_typeof(a);

var fn = function fn() {};

var mapData = ['1', '2'].map(function (item) {
  return item;
});

var Test1 = function Test1() {
  _classCallCheck(this, Test1);
};

var Test2 = function Test2() {
  _classCallCheck(this, Test2);
}; // Api


var c = [1, 2, 3].includes(1);
new Promise(function () {});

打包后代码体积为 263 kib(development模式下)

按需注入是可以了,到目前为止,上面的 babel 配置还存在两个问题:

  1. 对于例如includes等实例方法,直接在global.Array.prototype上添加。这样直接修改了全局变量的原型,有可能会带来意想不到的问题。这个问题在开发第三方库的时候尤其重要,因为我们开发的第三方库修改了全局变量,有可能和另一个也修改了全局变量的第三方库发生冲突,或者和使用我们的第三方库的使用者发生冲突。公认的较好的编程范式中,也不鼓励直接修改全局变量、全局变量原型。
  2. babel 转译 syntax 时,有时候会使用一些辅助的函数来帮忙转。class 语法中,babel 自定义了_classCallCheck这个函数来辅助;typeof 则是直接重写了一遍,自定义了_typeof这个函数来辅助。这些函数叫做 helpers。从上面代码中可以看到,helper 直接在转译后的文件里被定义了一遍。如果一个项目中有100个文件,其中每个文件都写了一个 class,那么这个项目最终打包的产物里就会存在100个_classCallCheck函数,他们的长相和功能一模一样,这显然不合理。

使用 @babel/plugin-transform-runtime 这个插件就能很好的解决上面提到的两个问题。

$ npm install --save-dev @babel/plugin-transform-runtime
$ npm install -D @babel/runtime-corejs3

修改.bablerc文件

{
	"presets": [
        [
            "@babel/preset-env"
        ]
    ],
    "plugins": [
        [
            "@babel/plugin-transform-runtime",
            {
                "corejs": 3
            }
        ]
    ]
}

打包后

__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _babel_runtime_corejs3_helpers_classCallCheck__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime-corejs3/helpers/classCallCheck */ "./node_modules/@babel/runtime-corejs3/helpers/classCallCheck.js");
/* harmony import */ var _babel_runtime_corejs3_helpers_classCallCheck__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_babel_runtime_corejs3_helpers_classCallCheck__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var _babel_runtime_corejs3_core_js_stable_instance_map__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @babel/runtime-corejs3/core-js-stable/instance/map */ "./node_modules/@babel/runtime-corejs3/core-js-stable/instance/map.js");
/* harmony import */ var _babel_runtime_corejs3_core_js_stable_instance_map__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_babel_runtime_corejs3_core_js_stable_instance_map__WEBPACK_IMPORTED_MODULE_1__);
/* harmony import */ var _babel_runtime_corejs3_core_js_stable_instance_includes__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @babel/runtime-corejs3/core-js-stable/instance/includes */ "./node_modules/@babel/runtime-corejs3/core-js-stable/instance/includes.js");
/* harmony import */ var _babel_runtime_corejs3_core_js_stable_instance_includes__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_babel_runtime_corejs3_core_js_stable_instance_includes__WEBPACK_IMPORTED_MODULE_2__);
/* harmony import */ var _babel_runtime_corejs3_core_js_stable_promise__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! @babel/runtime-corejs3/core-js-stable/promise */ "./node_modules/@babel/runtime-corejs3/core-js-stable/promise.js");
/* harmony import */ var _babel_runtime_corejs3_core_js_stable_promise__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_babel_runtime_corejs3_core_js_stable_promise__WEBPACK_IMPORTED_MODULE_3__);

var _context, _context2;

// Syntax
var a = 10;

var fn = function fn() {};

var mapData = _babel_runtime_corejs3_core_js_stable_instance_map__WEBPACK_IMPORTED_MODULE_1___default()(_context = ['1', '2']).call(_context, function (item) {
  return item;
});

var Test1 = function Test1() {
  _babel_runtime_corejs3_helpers_classCallCheck__WEBPACK_IMPORTED_MODULE_0___default()(this, Test1);
};

var Test2 = function Test2() {
  _babel_runtime_corejs3_helpers_classCallCheck__WEBPACK_IMPORTED_MODULE_0___default()(this, Test2);
}; // Api

var c = _babel_runtime_corejs3_core_js_stable_instance_includes__WEBPACK_IMPORTED_MODULE_2___default()(_context2 = [1, 2, 3]).call(_context2, 1);

new _babel_runtime_corejs3_core_js_stable_promise__WEBPACK_IMPORTED_MODULE_3___default.a(function () {});

打包后代码体积为 353 KiB(development模式下)
在引入了 transform-runtime 这个插件后:

  • Api 从之前的直接修改原型改为了从一个统一的模块中引入,避免了对全局变量及其原型的污染,解决了第一个问题
  • helpers 从之前的原地定义改为了从一个统一的模块中引入,使得打包的结果中每个 helper 只会存在一个,解决了第二个问题

样式处理

css-loader 会对 @import 和 url() 进行处理,就像 js 解析 import/require() 一样,然后分析 css 模块之间的依赖关系,合成⼀个 css
style-loader 把 css-loader 处理后的内容,动态创建 <style> 挂载到⻚⾯的 <head>

$ npm install style-loader css-loader -D

webpack.base.conf.js文件中的module.rules数组新增对象

{
    test: /\.css$/,
    use: ["style-loader", "css-loader"]
}

这里做一些优化

  • 使用插件mini-css-extract-plugin将样式抽离到一个样式文件里
$ npm install mini-css-extract-plugin -D
  • 使用PostCss为 CSS 规则添加特定浏览器厂商的前缀。Autoprefixer自动获取浏览器的流行度和能够支持的属性,并根据这些数据帮你自动为 CSS 规则添加前缀。
$ npm install postcss-loader@4 autoprefixer@9 cssnano@4 -D

在项目根目录下创建postcss.config.js

module.exports = {
    plugins: [
        require("autoprefixer"),
        require("cssnano"),
    ]
};

修改webpack.base.config.js文件

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
  module: {
    rules:[
        {
          test: /\.css$/,
          use: [
              // css不在直接挂载到页面中,而是分离出放到单独一个css文件中,这时候就不需要style-loader
              MiniCssExtractPlugin.loader,
              'css-loader',
              'postcss-loader'
          ],
      },
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css', // 输出⽂件的名字
    }),
  ]
};

修改main.js

import "./index.css"

创建./src/index.css

div {
    display: flex;
}

打包后得到 bundle.css

div{display:-webkit-box;display:-ms-flexbox;display:flex}

文件资源处理(图片、音频、视频、字体等)

file-loader
将一个本地的资源文件中的 import/require() 解析为 url,并且将文件发送到输出文件夹,再根据配置修改文件引用的路径,使资源在运行过程中能正确加载。

安装

$ npm install file-loader -D

webpack.base.config的module.rules添加

{
    test: /\.(png|jpe?g|gif|webp|svg|eot|ttf|woff|woff2)$/,
    use: [
      {
        loader: 'file-loader'
        options: {
            outputPath: 'assets', // 输出文件目录
            name: "[name]_[hash:6].[ext]" // 输出文件名
        },
      }
    ],
    exclude: /node_modules/
}

修改./main.js

import photo from './photo.jpg';
const img = new Image();
img.src = photo;
document.body.appendChild(img);

最终会把处理过的图片放到./dist/assets目录下

url-loader
file-loader功能类似,只是在⽂件⼤⼩⼩于指定的限制时会变成将文件编码返回DataURL直接放到主体文件,不会输出真实的文件。页面如果加载太多的图片资源,会发送很多的http请求,从而降低页面性能,这时候可以把文件较小的资源直接主体文件中,这样可以减少昂贵的⽹络请求。

注:设置 limit 可以让图片大小小于设定值转成 base64 码,而不是直接输出文件,但是limit 的设置要合理,太⼤会导致 JS ⽂件变大(主要是文件编码后所增加的体积都由 JS 文件承担)而加载变慢,需要兼顾加载速度和⽹络请求次数。如果需要使⽤图⽚压缩功能,可以使⽤image-webpack-loader

安装

$ npm install url-loader -D

webpack.base.config 修改 module.rules

{
    test: /\.(eot|ttf|woff|woff2)$/,
    use: [
      {
        loader: 'file-loader'
        options: {
            outputPath: 'assets', // 输出文件目录
            name: "[name]_[hash:6].[ext]" // 输出文件名
        },
      }
    ],
    exclude: /node_modules/
},
{
    test: /\.(png|jpe?g|gif|webp|svg)$/,
    use: [
      {
        loader: 'url-loader'
        options: {
            limit: 10240, // ⼩于此值的⽂件会被转换成DataURL
            outputPath: 'assets', // 输出文件目录
            name: "[name]_[hash:6].[ext]" // 输出文件名
        },
      }
    ],
    exclude: /node_modules/
}

这里做了一下处理,因为字体资源不需要编码,所以用file-loader处理。然后咱们把匹配视频、音频也纳入进来。

{
  test: /\.(eot|ttf|otf|woff2?)(\?\S*)?$/,
  use: [
    {
      loader: 'file-loader',
      options: {
        outputPath: 'assets',
      },
    }
  ],
  exclude: /node_modules/
},
{
  test: /\.(png|jpe?g|gif|webp|svg)(\?.*)?$/,
  use: [
    {
        loader: "url-loader",
        options: {
            limit: 10240,
            name: "[name]_[hash:6].[ext]",
            outputPath: "assets",
        },
    },
  ],
  exclude: /node_modules/
},
{
  test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
  use: [
    {
      loader: "url-loader",
      options: {
        limit: 4096,
        name: "[name]_[hash:6].[ext]",
        outputPath: "assets",
        publicPath: (url, resourcePath, context) => {
          return `../wp-content/plugins/vue-create-post-type/assets/${url}`
        }
      },
    },
  ],
  exclude: /node_modules/,
},

优化

HMR (Hot Moudle Replacement)

模块热替换(HMR - hot module replacement)功能会在应用程序运行过程中,替换、添加或删除 模块,而无需重新加载整个页面。 修改 webpack.dev.config.js 的devServer.hot

devServer: {
    contentBase: path.resolve(__dirname, '../dist'),
    open: true,
    hot: true
}

SplitChunksPlugin

一般项目代码包含了自身代码和第三方代码,一般webpack打包时会将这两种代码打包成一个bundle文件,浏览器再通过http请求bundle文件,如果文件太大,就会导致加载时间过长,从而影响网站首屏加载得时间。大家都知道浏览器有并发请求得机制,这样如果把体积大的bundle文件分为多块,让浏览器并行加载不就可以了吗?

CommonsChunkPlugin是用来提取第三方库和公共模块,避免重复依赖的插件,如果bundle文件过大就会分包并把一些非自身代码抽离出bundle文件,减少bundle文件的体积,从而提高加载速度。但是CommonsChunkPlugin还有一个问题是处理不了的,就是当多入口时,重复引用第三方块,第三方块会被打包进多次。

SplitChunksPlugin就很好的解决上面的问题,不仅继承了CommonsChunkPlugin该有的功能,还支持对重复模块进行缓存处理,避免重复打包。

Since version 4 the CommonsChunkPlugin was removed in favor of optimization.splitChunks and optimization.runtimeChunk options. Here is how the new flow works.

$ npm install terser-webpack-plugin@4 --save-dev
$ npm install css-minimizer-webpack-plugin --save-dev

webpack.base.config 添加 module.optimization

const TerserPlugin = require("terser-webpack-plugin");

...

// 优化
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendors: {
          name: 'chunk-vendors',
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          chunks: 'initial'
        },
        common: {
          name: 'chunk-common',
          minChunks: 2,
          priority: -20,
          chunks: 'initial',
          reuseExistingChunk: true
        }
      }
    },
    minimizer: [
        new TerserPlugin(
        {
          parallel: true,
          extractComments: false,
          terserOptions: {
            compress: {
              arrows: false,
              collapse_vars: false,
              comparisons: false,
              computed_props: false,
              hoist_funs: false,
              hoist_props: false,
              hoist_vars: false,
              inline: false,
              loops: false,
              negate_iife: false,
              properties: false,
              reduce_funcs: false,
              reduce_vars: false,
              switches: false,
              toplevel: false,
              typeofs: false,
              booleans: true,
              if_return: true,
              sequences: true,
              unused: true,
              conditionals: true,
              dead_code: true,
              evaluate: true
            },
            safari10: true
          }
        }
      )
    ]
  },

webpack.dev.config 添加 module.optimization

const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

...

optimization: {
    minimizer: [
      new CssMinimizerPlugin(), // 生产环境
    ]
  }

以上,就可以用Webpack打包项目了,接下来构建Vue。

Vue

安装

你应该将 vue-loader 和 vue-template-compiler 一起安装——除非你是使用自行 fork 版本的 Vue 模板编译器的高阶用户:

$ npm install -D vue-loader vue-template-compiler vue-style-css

每个 vue 包的新版本发布时,一个相应版本的 vue-template-compiler 也会随之发布。编译器的版本必须和基本的 vue 包保持同步,这样 vue-loader 就会生成兼容运行时的代码。这意味着你每次升级项目中的 vue 包时,也应该匹配升级 vue-template-compiler。

配置

webpack.base.config 添加 module.rules

const { VueLoaderPlugin } = require('vue-loader')

module.exports = {
  module: {
    rules: [
      // ... 其它规则
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      {
          test: /\.css$/,
          use: [
              // 插件需要参与模块解析,须在此设置此项,不再需要style-loader
              process.env.NODE_ENV !== 'production'
              ? 'vue-style-loader'
              : MiniCssExtractPlugin.loader,
              'css-loader',
              'postcss-loader'
          ],
      },
    ]
  },
  plugins: [
    // 请确保引入这个插件!
    new VueLoaderPlugin()
  ]
}

测试

安装 vue

$ npm install vue -S

./src/main.js

import Vue from 'vue'
import App from './view/App.vue'

new Vue({
    el: "#app",
    render: (h) => h(App)
})

./view/App.vue

<template>
    <div>
        Hello Vue!
    </div> 
</template>

总结

到此基本的 vue 打包环境完成了,有些配置没有解释清除,是因为我解释的没有文档好,大伙还是转驾到webpack。在配置时出现安装包报错绝大部分都是版本的问题,某些依赖包是配合使用的。webpack配置项很多,但不一定都要配置,因为webpack已经有默认的配置帮忙处理。最最最重要的是要看文档、看文档、看文档,特别是英文文档。