webpack从0到1构建

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

绝大部分生产项目都是基于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


mode


3、mode模式,主要是开发模式和生产模式两种模式,在生产模式webapck打包会默认压缩


module


4、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


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

const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
  plugins: [new HtmlWebpackPlugin({template: './src/index.html'})]
}


mode


6、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-devwebpack5中我们默认新建一个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

31c51c598b97b4c1427ca05b638acc73.png

并且控制台上已经成功了
> webpack
asset app.bundle.js 151 bytes [emitted] [minimized] (name: app)
./src/app.js 29 bytes [built] [code generated]
webpack 5.72.1 compiled successfully in 209 ms

我们打开一下生成的app.bundle.js,我们发现是这样的,这是在mode: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"" ""./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[1]直接打包指定的文件输出到对应的文件下

"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这样的文件,通过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】插件

4277812f9b1eadd7d659a229ca2e282d.png

但是,当我每次修改js文件,我都要每次执行npm run build这个命令,这就有些繁琐了,而且我本地是安装vsode插件的方式帮我打开页面的,这就有点坑了。


于是在webpack中就有一个内置cliwatch来监听文件的变化,我们只需要加上`--watch`[2]就可以了

"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`[3],我们常常称呼为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了

cd0ac841b82b663f4412cd3c9107ead1.png

模块热更新(Hot Module Replacement)


现在当我每次修改文件时,整个文件都会重新build,并且是在虚拟内存中引入,如果修改的只是部分文件,全部文件重新加载就有些浪费了,因此需要HMR,模块热更新devServer hot[4],在运行时更新某个变化的文件模块,无需全部更新所有文件

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

当我添加完后,发现热更新还是和以前一样,没什么用,官方这里有解释hot-module-replacement[5],通俗讲就是要指定某些文件要热更新,不然默认只要文件发生更改就得全部重新编译,从而全站刷新。


写了一段测试代码

// 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时,会有一个请求

b7ccc5afbace92754c7d80973749d9d7.png

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


webpack-dev-server内置了HMR,我们用webpack server这个命令就启动静态服务了,并且还内置了HMR,如果我不想用命令呢,我们可以通过API的方式启动dev-server(https://www.webpackjs.com/guides/hot-module-replacement/#%E5%90%AF%E7%94%A8-hmr "" "")


具体示例代码如下,新建一个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[6]


加载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才能使用,具体参考图片[7]


首先需要安装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`[8],输出的图片文件可以加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编译入口的基本配置,entryoutputmoduleplugins以及利用devServer开启热更新,并且使用module.hot.accept('path')实现HMR模块热替换功能


3、我们了解在命令行webpack --watch可以做到实时监听文件的变化,每次文件变化,页面都会重新加载


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


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


6、本文示例code-example[9]


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

相关文章
|
18天前
|
缓存 监控
webpack 提高构建速度的方式
【10月更文挑战第23天】需要根据项目的具体情况和需求,综合运用这些方法,不断进行优化和改进,以达到最佳的构建速度和效果。同时,随着项目的发展和变化,还需要持续关注和调整构建速度的相关措施,以适应不断变化的需求。
|
18天前
|
存储 缓存 前端开发
利用 Webpack 5 的持久化缓存来提高构建效率
【10月更文挑战第23天】利用 Webpack 5 的持久化缓存是提高构建效率的有效手段。通过合理的配置和管理,我们可以充分发挥缓存的优势,为项目的构建和开发带来更大的便利和效率提升。你可以根据项目的实际情况,结合以上步骤和方法,进一步优化和完善利用持久化缓存的策略,以达到最佳的构建效果。同时,不断探索和实践新的方法和技术,以适应不断变化的前端开发环境和需求。
|
5月前
|
缓存 JavaScript 前端开发
探讨如何通过一系列优化策略来提升TypeScript与Webpack的构建性能。
【6月更文挑战第11天】本文探讨了优化TypeScript与Webpack构建性能的策略。理解Webpack的解析、构建和生成阶段是关键。优化包括:调整tsconfig.json(如关闭不必要的类型检查)和webpack.config.js选项,启用Webpack缓存,实现增量构建,代码拆分和懒加载。通过这些方法,可以提升构建速度,提高开发效率。
79 0
|
3月前
|
前端开发 JavaScript C++
【绝技大公开】Webpack VS Rollup:一场前端工程化领域的巅峰对决,谁能笑到最后?——揭秘两大构建神器背后的秘密与奇迹!
【8月更文挑战第12天】随着前端技术的发展,模块化与自动化构建成为标准实践。Webpack与Rollup作为主流构建工具,各具特色。Webpack是一款全能型打包器,能处理多种静态资源,配置灵活,适合复杂项目;Rollup专注于ES6模块打包,利用Tree Shaking技术减少冗余,生成更精简的代码。Rollup构建速度快,配置简洁,而Webpack则拥有更丰富的插件生态系统。选择合适的工具需根据项目需求和个人偏好决定。两者都能有效提升前端工程化水平,助力高质量应用开发。
41 1
|
3月前
|
JavaScript 前端开发 API
解锁前端开发新境界:Vue.js携手Webpack,打造高效构建流程,你的项目值得拥有!
【8月更文挑战第30天】随着前端技术的发展,模块化与组件化趋势愈发显著。Vue.js 以其简洁的 API 和灵活的组件系统,深受开发者喜爱;Webpack 则凭借强大的模块打包能力成为前端工程化的基石。两者结合,不仅简化了组件编写与引用,还通过模块热替换、代码分割等功能大幅提升开发效率。本文将通过具体示例,展示如何利用 Vue.js 和 Webpack 构建高效、有序的前端开发环境。从安装配置到实际应用,逐步解析这一组合的优势所在。
49 0
|
5月前
|
缓存 前端开发 JavaScript
Webpack作为模块打包器,为前端项目提供了高度灵活和可配置的构建流程
【6月更文挑战第12天】本文探讨了优化TypeScript与Webpack构建性能的策略。理解Webpack的解析、构建和生成阶段是关键。优化包括:调整tsconfig.json(如关闭不必要的类型检查)和webpack.config.js选项,启用Webpack缓存,实现增量构建,代码拆分和懒加载。这些方法能提升构建速度,提高开发效率。
61 3
|
6月前
|
缓存 JavaScript 前端开发
【TypeScript技术专栏】TypeScript与Webpack构建优化
【4月更文挑战第30天】本文探讨了优化TypeScript与Webpack构建性能的策略。理解Webpack的解析、构建和生成阶段是关键。优化包括:调整tsconfig.json(关闭不必要的类型检查,适配目标环境)和webpack.config.js(配置entry、output、resolve,使用压缩插件)。启用Webpack缓存和增量构建,利用代码拆分与懒加载,能有效提升构建速度和开发效率。
87 0
|
6月前
|
缓存 前端开发 JavaScript
|
6月前
|
前端开发 JavaScript 开发者
webpack打包机制,构建过程和配置
webpack打包机制,构建过程和配置
46 0
|
6月前
|
JavaScript 前端开发 Windows
《Webpack5 核心原理与应用实践》学习笔记-> 构建Electron
《Webpack5 核心原理与应用实践》学习笔记-> 构建Electron
82 1