说说为何项目需要使用webpack打包

一、项目开发结构的演变

1、传统前端项目

传统前端项目中,使用引用script和link css来关联脚本和css文件,然后使用跳转链接来关联项目中的其他页面,从而形成整个网站访问的结构树。

2、依赖es6 module能力的模块化项目

当转变为模块化项目 时,可以在<script type="module">中通过import引入其他module的js。

仅仅依靠js原生esm的module功能,

能实现:

  1. js分模块,通过导入来关联

不能实现:

  1. 把html的内容分模块,然后组合成一个完整的html
  2. css也没法分模块
  3. 文件名称必须时html结尾(只能写原生3大件),无法适配各种各样的其他语法的模板内容,也就意味着没有其他能力(比如Sass,比如vue模板文件,其他工程化的内容)

3、工程化项目

随心所欲的使用各种模板语言,

比如Sass(通过Sass编译器转译为css后就可以整合到原始html中),

比如.vue类型文件,通过vue的编译器解析就可以生成html,

二、原始前端项目工程化遇到的难题

但是,

工程化项目一般是依赖node环境,通过npm工具管理包依赖,

工程化项目中使用的各种语言都各自有自己的编译器(假设),难道手动去给每个编译器运行一下?

node中依赖的各种关系,离开了node环境发布到浏览器之后怎么运行?怎么把这些依赖关系转变为html或者原生js中可识别的代码?

node工程化项目极大的扩展了代码能力,比如可以通过引入babel插件在编译期间去polyfill es6的功能使得兼容es5,难道这个插件也要每次发布都自己运行一次吗?

有没有一种统一的方式,通过配置的方式,帮我们把以上所有这些功能都处理了?

让我们自由的开发,安心的部署?

接下来,就是webpack登场了!!!

三、webpack如何把工程化的多模块,转化为一个js文件

假如有a(入口模块)、b、c三个模块,有依赖关系,如何转化成js?

下面伪代码展示webpack打包合并基本原理:

js 复制代码
(function(...abc){
   //执行入口aFunc
   aFunc(abc)
})(aFunc,bFunc,cFunc);

解析:

  1. 把每个模块处理成一个函数,这样各个模块作用域之间就不会存在变量冲突
  2. 打包出来的js文件就是一个立即执行函数,也不会污染全局变量
  3. 从入口模块aFunc开始执行,如果候aFunc内部依赖b模块,就会:
js 复制代码
function aFunc(abc){
    //a依赖b
    abc.b();
}

上面描述了大概原理,下面是比较准确的代码解析:

第一步,把多个模块编译为多个函数,这样内部变量不会冲突

js 复制代码
const 多个模块 = {
  './src/a.js': function (module, exports) {
    console.log('module a');
    module.exports = 'a';
  },
  './src/index.js': function (module, exports) {
    console.log(' index module');
    var a = require('./src/a.js');
    console.log(a);
  },
};

第二步,模块依赖使用一个require函数表示,使用module变量接收模块的导出

js 复制代码
function require(moduleId) {
  var func = 多个模块[moduleId];
  var module = {
    exports: {},
  };
  func(module, module.exports);
}

第三步,一个立即执行函数,从入口模块开始执行

js 复制代码
(function (多个模块) {
  //执行入口模块
  require('./src/index.js');
})(多个模块);

第四步,把前面三步 的代码合并为一个马上传参的立即执行函数,就不会存在任何全局变量:

js 复制代码
(function (多个模块) {

  function require(moduleId) {}// 函数写内部
  
  //执行入口模块
  require('./src/index.js');
})(直接传参:{多个模块});

四、在webpack中使用自定义loader

webpack.config.js配置如下

js 复制代码
const path = require('path');

module.exports = {
  mode: 'development',
  entry: {
    main: './src/main.js',
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js',
    clean: {
      keep: /\.html$/,
    },
  },
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: [
          'css-loader',
          {
            loader: './loaders/my-sass-loader',
          },
        ],
      },
    ],
  },
};

解析:

  1. entry入口'./src/main.js'
  2. output.path 打包位置dist
  3. clean.keep: /.html$/ 不要清除dist中的html文件
  4. module.rules[0] ,模块匹配条件:test('.scss'), 匹配上之后使用use两个loader处理(css-loadermy-sass-loader
  5. css-loader是npm引入的包,my-sass-loader是自定义自己写的loader

my-sass-loader内容如下:

js 复制代码
module.exports = function (source) {
  const sass = require('sass');
  let result = sass.compileString(source);
  return result.css;
};

入口文件main.js内容如下

js 复制代码
import styleText from './assets/styles/index.scss';

var style = document.createElement('style');
style.innerHTML = styleText;
document.head.appendChild(style);

因为main.js依赖index.scss模块,index.scss被webpack的test: /\.scss$/后缀匹配,所以index.scss会被css-loadermy-sass-loader处理转换。

这里是递归调用loader :

css-loader pitch

my-sass-loader pitch

my-sass-loader

css-loader

所以,先执行pitch,递归回来时再执行loader function,因此loader的执行顺序是倒过来的

上面,在my-sass-loader中使用sass依赖包,把scss中的sass写法compileString成了css格式,再交由css-loader

最后,在main.js中,把转义后的styleText(css格式)添加到内部样式表《style》中

最后,在html中引用main.js:

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./main.js" defer></script>
  </head>
  <body>
    <div id="app">这是一个将要应用样式的文字</div>
  </body>
</html>

至此,一个简单的sass转义loader完成了!

更进一步:把sass文件转化成css文件

bash 复制代码
pnpm install extract-loader file-loader  --save-dev
js 复制代码
const path = require('path');

module.exports = {
  devtool: 'source-map',
  mode: 'development',
  entry: {
    main: './src/main.js',
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    // publicPath: 'dist/',
    filename: '[name].js',
    clean: {
      keep: /\.html$/,
    },
  },
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: 'assets/[name].css',
            },
          },
          {
            loader: 'extract-loader',
            options: {
              // publicPath: '../',
            },
          },
          {
            loader: 'css-loader',
          },
          {
            loader: './loaders/my-sass-loader',
          },
        ],
      },
    ],
  },
};

五、webpack中使用babel-loader把es6转成es5

babel是什么?

开始

安装babel开发依赖:

pnpm install -D @babel/core @babel/preset-env

pnpm i core-js@3

babel配置文件babel.config.js

js 复制代码
module.exports = {
  presets: [
    [
      '@babel/env',
      {
        targets: {
          browsers: ['> 0.2%  ', 'last 4 versions', 'not ie <=8'],
        },
        corejs: '3',
        useBuiltIns: 'usage',
      },
    ],
  ],
};

自定义一个loader:

添加到webpack.config.js

js 复制代码
    rules: [
        
        {
          test: /\.js$/,
          use: [
            {
              loader: './loaders/my-babel-loader',
              options: {},
            },
          ],
        },
      ],

my-babel-loader.js内容:

js 复制代码
const babel = require('@babel/core');

module.exports = function (source) {
  let { code: result, map, ast } = babel.transformSync(source, {});
  return result;
};

至此,自定义babel-loader调用babel-core能力转义es6为es5成功。

六、webpack使用自定义plugin,把第步中loader生成的css文件关联到index.html

安装html的node环境解析器: pnpm install cheerio -D

webpack.config.js中配置添加自定义的插件:

js 复制代码
const MyPlugin = require('./plugins/MyPlugin');

module.exports = {
  plugins: [new MyPlugin(null)],
}

项目目录结构如下:

arduino 复制代码
项目
├── plugins
│   └── MyPlugin.js
│ 
├── public
│   └── index.html
│ 
├── src
│   ├── index.js
│   └── 。。。
│ 
├──webpack.config.js

解析插件要做什么:

  1. public/index.html文件读取出来
  2. 把webpack生成的css文件,自动注入到public/index.html中(需要用到cheerio
  3. public/index.html文件写入到打包的output输出位置中

MyPlugin.js内容如下:

js 复制代码
const fs = require('fs');
const cheerio = require('cheerio');
const webpack = require('webpack');

module.exports = class MyPlugin {
  constructor(param) {
    this.param = param;
  }

  apply(compiler) {
    //emit事件触发:打包,在内存中生成了文件,在写文件到磁盘之前
    compiler.hooks.compilation.tap('compilation', (compilation) => {
      const options = {
        name: 'MyPlugin',
        stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
      };

      compilation.hooks.processAssets.tap(options, myPlugin_emit);

      function myPlugin_emit(assets) {
        let cssFilePathList = Object.entries(assets)
          .map(([pathname, source]) => pathname)
          .filter((pathname) => pathname.endsWith('.css'));

        //读取index.html并解析成cheerio对象
        const indexHtml = fs.readFileSync('public/index.html', 'utf8');
        const $ = cheerio.load(indexHtml);

        //添加css到index.html的link引用
        cssFilePathList.forEach((cssFilePath) => {
          $('head').append(`<link rel="stylesheet" href="${cssFilePath}">`);
        });
        const html = $.html();

        //添加到output
        compilation.emitAsset(
          'index.html',
          new webpack.sources.RawSource(html)
        );
      }
    });
  }
};

至此,dist目录下已经有一个关联了css文件的index.html

也可以使用style-loader把css样式注入到引用本js的html的head>style标签中

七、webpack的三个环境

  1. webpack本身代码的运行环境(node),连同webpack.config.js,各种插件都是在编译时的node程序中运行
  2. webpack以及插件编译过程的文本语法解析环境(项目中的源码,会被解析和转译,比如源码中的import语句,不是真正执行,而是处于编译环境中,被语法分析)
  3. 打包后的输出代码的运行环境(一般就是浏览器运行环境,在webpack编译过程中并不涉及最终运行环境,但是生成的代码的目标是运行在最终运行环境,所以es6转es5等等很多操作都是基于这个目标环境来编译的)

八、原理过程

  1. webpack根据依赖关系,递归从a找到b,从b找到c,解析每一个依赖(重复的不会再次解析)
  2. 解析每一个依赖,根据loader的匹配规则,匹配上的就交由loader转译(把所有其他什么jsx、ts、css等等,转译成webpack认识的js)
  3. 在整个生命周期的过程中,放置很多钩子,触发时调用插件plugin的对应方法(可以复制静态文件,分离文件,等等操作)
  4. 代码压缩、等等各种转换代码、转换文件的操作,最终得到想要的目标输出文件

九、webpack最佳实践(想要达到的目的)

  1. 使用vite,开箱即用,不用自己配置
相关推荐
vipbic6 小时前
关于Vue打包的遇到模板引擎解析的引号问题
前端·webpack
妮妮喔妮9 小时前
Webpack 有哪些特性?构建速度?如何优化?
前端·webpack·node.js
ST.J9 小时前
webpack笔记
前端·笔记·webpack
webYin1 天前
vue2 打包生成的js文件过大优化
前端·vue.js·webpack
!执行1 天前
webpack 相关配置
webpack
醉方休1 天前
vite与webpack对比
前端·webpack·devops
wallflower20201 天前
🚀 从 Webpack 到 Vite:企业级前端构建、代码分割与懒加载优化完全指南
webpack·vite
一枚前端小能手1 天前
🚀 Webpack打包慢到怀疑人生?这6个配置让你的构建速度起飞
前端·javascript·webpack
全栈技术负责人2 天前
webpack性能优化指南
webpack·性能优化·devops
和雍2 天前
webpack5 创建一个 模块需要几步?
javascript·面试·webpack