乐趣区

关于javascript:webpack从0到1构建

绝大部分生产我的项目都是基于 cli 脚手架创立一个比较完善的我的项目,从晚期的 webpack 配置工程师到前面的无需配置,大大解放了前端工程建设。然而时常会遇到,不依赖成熟的脚手架,从零搭过我的项目吗,有遇到哪些问题吗?或者有理解 loaderplugin吗?如果只是应用脚手架,作为一个深耕业务一线的工具人,什么?还要本人搭?还要写loader, 这就过分了。

注释开始 …

前置

咱们先理解下 webpack 能干什么

webpack是一个动态打包工具,依据入口文件构建一个依赖图,依据须要的模块组合成一个 bundle.js 或者多个bundle.js, 用它来展现动态资源

对于 webpack 的一些外围概念,次要有以下,参考官网

entry

1、entry入口(依赖入口文件,webpack 首先依据这个文件去做外部模块的依赖关系)

// webpack.config.js
module.exports = {entry: './src/app.js'}
// or
/*
// 是以下这种形式的简写 定义一个别名 main
module.exports = {
  entry: {main: ./src/app.js'}
}
*/

也能够是一个数组

// webpack.config.js
module.exports = {entry: ['./src/app.js', './src/b.js'],
  vendor: './src/vendor.js'
}

在拆散利用 app.js 与第三方包时,能够将第三方包独自打包成 vender.js, 咱们将第三方包打包成一个独立的chunk, 内容hash 值放弃不变,这样浏览器利用缓存加载这些第三方js,能够缩小加载工夫,进步网站的访问速度。

不过目前 webpack4.0.0 曾经不倡议这么做,次要能够应用 optimization.splitChunks 选项,将 appvendor会分成独立的文件,而不是在入口处创立独立的entry

output

2、output输入 (把依赖的文件输入一个指定的 目录 下)

次要会依据 entry 的入口文件名输入到指定的文件名目录中, 默认会输入到 dist 文件中

const path = require('path');
// webpack.config.js
module.exports = {
 entry: {app: './src/app.js'},
 output: {path:  path.resolve(__dirname, 'dist'),
    filename: '[name].bundle.js'
  }
}
/*
module.exports = {
   entry: './src/app.js',
   output: {filename: '[name].bundle.js'
   }
}
*/
// 默认输入 /dist/app.bundle.js

module

3、module 配制 loader 插件,loader能让 webpack 解决各种文件,并把文件转换为可依赖的模块,以及能够被增加到依赖图中。其中 test 是匹配对应文件类型,use是该文件类型用什么 loader 转换,在打包前运行。

module.exports = {
  module: {
    rules: [
      {
        test: /\.less$/,
        use: 'less-loader'
      },
      {
        test: /\.ts$/,
        use: 'ts-loader'
      },
      {
        test: /\.css$/,
        use: [
          {loader: 'style-loader'},
          {
            loader: 'css-loader',
            options: {modules: true}
          },
          {loader: 'sass-loader'}
        ]
      }
    ]
  }
}

plugins

4、plugins次要是在整个运行时都会作用,打包优化,资源管理,注入环境

const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {plugins: [new HtmlWebpackPlugin({template: './src/index.html'})]
}

mode

5、mode指定打包环境,developmentproduction, 默认是production

从零开始一个我的项目搭建

新建一个目录webpack-01, 执行npm init -y

npm init -y // 生成一个默认的 package.json

package.json 中配置scirpt

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

首先咱们在在开发依赖装置 webpackwebpack-cli, 执行 npm i webpack webpack-cli --save-dev
webpack5中咱们默认新建一个 webpack 的默认配置文件webpack.config.js

const path = require('path');
module.exports = {
  entry: {app: './src/app.js'},
  output: {filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
    libraryTarget: 'commonjs'
  },
  mode: 'production'
};

咱们在 src 目录下新建一个 app.js 并写入一段 js 代码

console.log('hello, webpack')

在终端执行 npm run build, 这个命令我在package.jsonscript中配置

  "scripts": {
    "test": "echo \"Error: no test specified\"&& exit 1",
    "build": "webpack",
    "build:test_dev": "webpack --config webpack_test_dev_config.js",
    "build:test_prd": "webpack --config webpack_test_prd_config.js",
    "build:default": "webpack --config webpack.config.js",
    "build:o": "webpack ./src/app.js -o dist/app.js"
  },

此时就会生成一个在 dist 文件, 并且名字就是 app.bundle.js

并且管制台上曾经胜利了

webpack
asset app.bundle.js 151 bytes [emitted] [minimized] (name: app)
./src/app.js 29 bytes [built] 
webpack 5.72.1 compiled successfully in 209 ms

咱们关上一下生成的 app.bundle.js, 咱们发现是这样的,这是在model:production 下生成的一个匿名的自定义函数。

// app.bundle.js
(() => {var e = {};
  console.log(3), console.log('hello, webpack');
  var o = exports;
  for (var l in e) o[l] = e[l];
  e.__esModule && Object.defineProperty(o, '__esModule', { value: !0});
})();

这是生产环境输入的代码,就是在一个匿名函数中输入了后果,并且在 {} 上绑定了一个 __esModule 的对象属性,有这样一段代码 var o = exports; 次要是因为咱们在 output 中新增了 libraryTarget:commonjs, 这个会决定js 输入的后果。

咱们再来看下如果 mode:development 那么是怎么样

// 这是在 mode: development 下生成一个 bundle.js

/*
 * ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
 * This devtool is neither made for production nor for readable output files.
 * It uses "eval()" calls to create a separate source file in the browser devtools.
 * If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
 * or disable the default devtool with "devtool: false".
 * If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
 */
/******/ (() => { // webpackBootstrap
/******/     var __webpack_modules__ = ({

/***/ "./src/app.js":
/*!********************!*\
  !*** ./src/app.js ***!
  \********************/
/***/ (() => {eval("\nfunction twoSum(a, b) {\n  return a+b\n}\nconst result = twoSum(1,2);\nconsole.log(result);\nconsole.log('hello, webpack');\n\n//# sourceURL=webpack://webpack-01/./src/app.js?");

/***/ })

/******/     });
/************************************************************************/
/******/     
/******/     // startup
/******/     // Load entry module and return exports
/******/     // This entry module can't be inlined because the eval devtool is used.
/******/     var __webpack_exports__ = {};
/******/     __webpack_modules__["./src/app.js"](""./src/app.js"");
/******/     
/******/ })()
;

这下面的代码就是运行 mode:development 模式下生成的, 简化一下就是

(() => {
  var webpackModules = {'./src/app.js': () => evel('app.js 外部的代码')
  }
  weboackModules['./src/app.js']("'./src/app.js'");
})()

在开发环境就是会以文件门路为 key, 而后通过evel 执行 app.js 的内容,并且调用这个 webpackModules 执行 evel 函数

留神咱们默认 libraryTarget 如果不设置,那么就是var,次要有以下几种amdcommonjs2,commonjs,umd

通过以上,咱们会发现咱们能够用配置不同的命令执行打包不同的脚本,在默认状况下,npm run build与执行 npm run build:default 是等价的,咱们会看到 default--config webpack.config.js指定了 webpack 打包的环境的自定义配置文件。

如果配置默认文件名就是 webpack.config.js 那么 webpack 就会依据这个文件进行打包,webpack --config xxx.js是指定自定义文件让 webpack 依据 xxx.js 输出与输入的文件进行一系列操作。

  "scripts": {
    "test": "echo \"Error: no test specified\"&& exit 1",
    "build": "webpack",
    "build:default": "webpack --config webpack.config.js",
  },

除了以上,咱们能够不应用配置 webpack --config webpack.config.js 这个命令,而是间接在命令行 -cli 间接打包指定的文件输入到对应的文件下

 "scripts": {"build:o": "webpack ./src/app.js --output-path='./dist2'--output-filename='[name]_[hash].bundle.js'"
  },

会创立 dist2 目录并打包进去一个默认命名的 main_ff7753e9dbb1e41a06a6.bundle.js 的文件

咱们会发现咱们配置了诸如 webpack_test_dev_config.js 或者 webpack_test_prd_config.js z 这样的文件,通过build: test_devbuild:test_prd来辨别,外面文件内容仿佛大同小异,那么我可不可以复用一份文件,通过里面的环境参数来管制呢?这点在理论我的项目中会常常应用

环境参数

咱们能够通过 package.json 中指定的参数来确定, 能够用 --mode='xxx'--env a='xxx'

    "scripts": {"build2": "webpack --mode='production'--env libraryTarget='commonjs'--config webpack.config.js"},

此时 webpack.config.js 须要改成函数的形式
第二参数 argv 能获取全副的配置的参数

// webpack.config.js
const path = require('path');
module.exports = function (env, argv) {console.log(env, argv);
  return {
    entry: {app: './src/app.js'},
    output: {filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist'),
      library: 'MyTest',
      libraryTarget: argv.libraryTarget
    },
    mode: argv.mode
  };
};

因而咱们就能够通过批改 package.json 外面的变量,从而管制webpack.config.js

运行整个我的项目

咱们曾经创立了一个 src/app.js 的入口文件,当初须要在浏览器上拜访,因而须要构建一个 index.html, 在根目录中新建public/index.html, 并且引入我刚打包的js 文件

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>hello-webpack</title>
</head>
<body>
    <div id="app"></div>
   <script src="../dist/app.bundle.js"></script>
</body>
</html>

终于功败垂成,我关上浏览器,关上页面终于能够拜访了,【我本地装了 live server】插件

然而,当我每次批改 js 文件,我都要每次执行 npm run build 这个命令,这就有些繁琐了, 而且我本地是装置 vsode 插件的形式帮我关上页面的,这就有点坑了。

于是在 webpack 中就有一个内置 cliwatch 来监听文件的变动, 咱们只须要加上--watch 就能够了

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

这种形式会始终监听文件的变动,当文件发生变化时,就会从新打包,页面会从新刷新。

当然还有一种形式,就是能够在 webpack.config.js 中退出watch

// webpack.config.js
{
    watch: true,
    entry: {app: './src/app.js'},
}

而后咱们就改回原来的,将 --watch 去掉就行。

--watch这种形式的确晋升我本地开发效率,因为只有文件一发生变化,就会从新打包编译,联合 vscode 的插件就会从新加载最新的文件,然而随着我的项目的宏大,那么这种效率就很低了,因而除了 webpack 本身的 watch 计划,咱们须要去理解另外一个计划webpack-dev-server

webpack-dev-server

咱们须要借助一个十分弱小的插件工具来实现 本地动态服务 , 这个插件就是 webpack-dev-server,咱们经常称说为WDS 本地服务,他有热更新,并且浏览器会主动刷新页面,无需手动刷新页面

并且咱们还须要引入另一个插件 Html-webpack-plugins 这个插件,它能够主动帮咱们引入打包后的文件。当咱们启动本地服务,生地文件 js 文件会在内存中生成, 并且被 html 主动引入

咱们在 webpack.config.js 中引入html-webpack-plugin

const path = require('path');
// 引入 html-webpack-plugin
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = function (env, argv) {console.log(env);
  console.log(argv);
  return {
    entry: {app: './src/app.js'},
    output: {filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist'),
      library: 'MyTest',
      libraryTarget: argv.libraryTarget
    },
    mode: argv.mode,
    plugins: [new HtmlWebpackPlugin({template: './public/index.html'})]
  };
};

并且在 package.json 中减少 server 命令, 留神咱们加了 server,webpack-dev-server 外部曾经有对文件监听,当文件发生变化时,能够实时更新生成在内存的那个 js, 这个server 命令就是我装置的 webpack-dev-server 的命令

  "scripts": {"server": "webpack server"},

控制台运行 npm run server 默认关上 8080 端口,曾经 ok 了

模块热更新(Hot Module Replacement)

当初当我每次批改文件时,整个文件都会从新 build, 并且是在虚拟内存中引入,如果批改的只是局部文件,全副文件从新加载就有些节约了,因而须要HMR,热更新 devServer hot, 在运行时更新某个变动的文件模块,无需全副更新所有文件

// weboack.config.js
{
 mode: argv.mode,
 devServer: {hot: true},
}

当我增加完后,发现热更新还是和以前一样,没什么用,官网这里有解释 hot-module-replacement, 艰深讲就是要指定某些文件要热更新,不然默认只有文件产生更改就得全副从新编译,从而全站刷新。

写了一段测试代码

// utils/index
var str = '123';
function deepMerge(target) {console.log(target, '=22==');
  if (Array.isArray(target)) {return target;}
  const result = {};
  for (var key in target) {if (Reflect.has(target, key)) {if (Object.prototype.toString.call(target[key]) === '[object Object]') {result[key] = deepMerge(target[key]);
      } else {result[key] = target[key];
      }
    }
  }
  return result;
}
console.log('深拷贝一个对象 555', str);
export default deepMerge;
// module.exports = {
//   deepMerge
// };

app.js 中引入

import deepMerge from './utils/index';
// const {deepMerge} = require('./utils/index.js');
function twoSum(a, b) {return a + b;}
const userInfo = {
  name: 'Maic',
  age: 18,
  test: {book: 'webpack'}
};

const result = twoSum(1, 2);
console.log(result, deepMerge(userInfo));
if (module.hot) {
  // 这个文件
  module.hot.accept('./utils/index.js', () => {});
}
const str = 'hello, webpack322266666';
console.log(str);

const app = document.getElementById('app');

app.innerHTML = str;

留神咱们加了一段代码判断指定模块是否HMR

if (module.hot) {
  // 这个文件
  module.hot.accept('./utils/index.js', () => {});
}

这里留神一点 ,指定的utils/index.js 必须是 esModule 的形式输入,要不然不会失效
,咱们会发现,当我批改utils/index.js 时,会有一个申请

当你每改这个文件都会申请一个 app.[hash].hot.update.js 这样的一个文件。

webpack-dev-server内置了 HMR, 咱们用webpack server 这个命令就启动动态服务了,并且还内置了HMR, 如果我不想用命令呢,咱们能够通过 API 的形式启动dev-server[](https://www.webpackjs.com/gui... ""), 具体示例代码如下,新建一个config/server.js

const webpackDevServer = require('webpack-dev-server');

const webpack = require('webpack');

const config = require('../webpack.config.js');
const options = {hot: true, contentBase: '../dist', host: 'localhost'};
// 只能用 V2 版本 https://github.com/webpack/webpack-dev-server/blob/v2
webpackDevServer.addDevServerEntrypoints(config, options);

const compiler = webpack(config);

const server = new webpackDevServer(compiler, options);
const PORT = '9000';
server.listen(PORT, 'localhost', () => {console.log('server is start' + PORT);
});

webpack-dev-middleware 代替 webpack-dev-server

// config/server.js
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const app = express();

const config = require('../webpack_test_dev_config');
const compiler = webpack(config);
// 设置动态资源目录
app.use(express.static('dist'));
app.use(webpackDevMiddleware(compiler, {}));
const PORT = 8000;
app.listen(PORT, () => {console.log('server is start' + PORT);
});

而后命令行配置node config/server.js,能够参考官网 webpack-dev-middleware

加载 css[XHR 更新款式]

npm i style-loader css-loader --save-dev

配置加载 css 的loader

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

款式是内敛在 html 外面的,如何提取成单个文件呢?

mini-css-extract-plugin 提取 css

// webpack.config.js
const miniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = function (env, argv) {
    return {
          module: {
           rules: [
             {
               test: /\.css$/,
               // use: ['style-loader', 'css-loader']
              use: [
                miniCssExtractPlugin.loader,
                'css-loader'
              ]
             }
           ]
       },
        plugins: [
           new miniCssExtractPlugin({filename: 'css/[name].css'
           })
       ]
    }
}

咱们把 style-loader 去掉了,并且换成了 miniCssExtractPlugin.loader, 并且在plugins 中退出插件, 将 css 文件提取了指定文件中, 此时就会发现 index.html 内敛的款式就变成一个文件加载了。

图片资源加载

咱们只晓得 css 用了 css-loaderstyle-loader, 那么图片以及非凡文件也是须要非凡 loader 能力应用,具体参考图片

首先须要装置 file-loader 执行 npm i file-loader --save-dev

// webpack.config.js
{
   ...
    module: {
     rules: [
       {
         test: /\.css$/,
         use: [miniCssExtractPlugin.loader, 'css-loader']
       },
       {test: /\.(png|svg|jpg|gif|jpeg)$/,
         use: [
         {
             loader: 'file-loader',
             options: {
               outputPath: 'assets',
               name: '[name].[ext]'
             }
           }
         ]
       }
     ]
   }
 }

能够参考 file-loader,输入的图片文件能够加hash 值后缀,当打包上传后,如果文件没有更改,图片更容易从缓存中获取

app.js 中退出引入图片

import deepMerge from './utils/index';
import '../assets/css/app.css';
import image1 from '../assets/images/1.png';
import image2 from '../assets/images/2.jpg';
// const {deepMerge} = require('./utils/index.js');
function twoSum(a, b) {return a + b;}
const userInfo = {
  name: 'Maic',
  age: 18,
  test: {book: '公众号:Web 技术学苑'}
};

const result = twoSum(1, 2);
console.log(result, deepMerge(userInfo));
if (module.hot) {
  // 这个文件
  module.hot.accept('./utils/index.js', () => {});
}
const str = `<div>
      <h5>hello, webpack</h5>
      <div>
          <img src=${image1} />
      </div>
      <div>
        <img src=${image2} />
      </div>
    </div>`;
console.log(str);

const app = document.getElementById('app');

app.innerHTML = str;

看下引入的图片页面

功败垂成,css 图片 资源都曾经 OK 了

总结

1、理解 webpack 是什么,它次要是前端构建工程化的一个工具,将一些譬如 ts,sass,vue,tsx 等等一些浏览器无奈间接拜访的资源,通过 webpack 能够打包成最终浏览器能够拜访的 htmlcssjs 的文件。并且 webpack 通过一系列的插件形式,提供 loaderplugins
这样的插件配置,达到能够编译各种文件。

2、理解 webpack 编译入口的根本配置,entry,outputmoduleplugins以及利用 devServer 开启热更新,并且应用 module.hot.accept('path') 实现 HMR 模块热替换性能

3、咱们理解在命令行 webpack --watch 能够做到实时监听文件的变动,每次文件变动,页面都会从新加载

4、咱们学会如何应用加载 css 以及 图片资源 ,学会配置css-loader,style-loaderfile-loader, 以及利用min-css-extract-plugin 去提取 css, 用html-webpack-plugin 插件实现本地 WDS 动态文件与入口文件的映射,在 html 中会主动引入实时打包的入口文件的app.bundle.js

5、相熟从 0 到 1 搭建一个前端工程化我的项目

6、本文示例 code-example

下一节会基于这个当下我的项目搭建 vuereact 我的项目,以及我的项目的 tree-shaking, 懒加载 缓存 自定义 loader,plugins

退出移动版