Loading

Webpack笔记 一

我擦擦擦擦擦擦,前端也太难了吧。。

概述

Webpack是一个JS程序静态模块打包工具,从一个或多个入口点,根据项目中的依赖关系将整个项目中的每一个模块组合成一个或多个bundles。

入口(entry)

用于指定webpack从哪一个文件开始构建。

默认值./src/index.js,可以通过如下方式配置

module.exports = {
    entry: './path/to/my/entry/file.js'
}

输出(output)

指定webpack将所有依赖打包后输出的bundle位置。

默认值./dist/main.js,可以通过如下方式配置

const path = require('path');

module.exports = {
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'my-first-webpack.bundle.js'
    }
}

loader

loader给了webpack认识除了jsjson外其它文件的能力。

module.exports = {
    module: {
        rules: [{test: /\.txt$/, use: 'raw-loader'}]
    }
}

上面的意思就是当webpack打包时发现某个模块引入了.txt结尾的依赖,那么使用raw-loader进行加载。

plugin

插件则用来扩展webpack的能力,可以这样使用插件

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

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

mode

告诉webpack你当前运行在什么环境

module.exports = {
    mode: 'production'
}

babel介绍

babel是一个JavaScript编译器,webpack的某些工作依赖babel,它将JS语言编译为JS语言。。。

是将使用ES2015+标准编写的JS语言编译成兼容旧版本浏览器以及其他环境中能运行的JS的编译器。

JS语言的发展越来越快,很多新特性不断出现,但是如果你想让你的项目在当前还在流行的所有浏览器都能够运行的话,那么你得等不支持这个新特性的浏览器过时才能在代码中使用这个新特性。

JS社区采用的办法是使用语法转换和Polyfill来让新标准得以在旧浏览器上运行。

新标准通常有两种:

  1. 语法标准:比如import,export语句,这些需要通过语法转换转换成旧语法
  2. API标准:比如Promise API,这种只需要在打包后的代码中添加Polyfill代码段,提供一个行为一致的Promise API即可。

再解释一下Polyfill,比如旧版本中没有Object.assign,这时候只需要在你代码运行前保证下面这一个代码段被运行:

Object.prototype.assign = function() {
    // 实现assign功能的代码
}

这一段代码能保证你程序中对新API的调用都能够正确执行,这段代码就叫polyfill

资源

编写你自己的Webpack

感觉这个挺有用的,手把手写了个简单的打包工具,知道了打包工作的几个步骤

  1. 使用@babel/parser读取使用新标准编写的js模块化代码并构建AST
  2. 使用@babel/traversa遍历AST,读取每个文件的依赖
  3. 生成依赖图
  4. 遍历依赖图,使用@babel/core将每个模块的代码转换成兼容浏览器的代码
  5. 将转换后的代码所依赖的requiremodulesexports变量填入

起步

mkdir webpack-concept
npm init -y
npm install webpack webpack-cli --save-dev

现在创建如下项目结构

webpack-concept
|- package.json
|- /dist
    |- index.html
|- /src
    |- index.js

src/index.js为入口文件,dist为输出目录,我们要通过webpack把index.js打包到dist目录中。

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


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

  // lodash(目前通过一个 script 引入)对于执行这一行是必需的
  element.innerHTML = _.join(['Hello', 'webpack'], ' ');

  return element;
}

document.body.appendChild(component());

安装下lodash

npm install lodash --save

默认情况下,webpack会将编译后的js文件打包到dist/main.js,所以我们要在index.html里引用

<!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>Document</title>
</head>
<body>
    <script src="main.js"></script>
</body>
</html>

打包

npx webpack

这是一个简单的示例

  1. webpack会解析js文件中的import和export部分的语法部分,提供开箱即用的支持,其它部分不会更改,如果使用其它的ES2015+特性,请确保你使用了一个如babel一样的loader
  2. webpack4支持无配置直接打包

配置文件

编写一个和默认行为一致的配置文件

// webpack.config.js
const path = require('path');

module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'main.js',
        path: path.resolve(__dirname, 'dist')
    }
}

管理资源

webpack把项目中的每一个文件都视作一个依赖,并动态解析每个地方引入的依赖,将它们打包,避免了无用资源的打包。

它的一个好处就是,它不仅处理js文件,还处理任何类型的文件,只要你为它添加一个loader。

加载css

npm install --save-dev style-loader css-loader
const path = require('path');

module.exports = {
    entry: './src/index.js',
    output: {
        // [+] 注意这里把之前的`main.js`改为了`bundle.js`,相同的更改要反映到`dist/index.html`中
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    },
    module: {
        rules: [
            {test: /\.css$/i, use: ['style-loader', 'css-loader']}
        ]
    }
}

通过module.rules可以使用正则表达式匹配不同的文件名指定不同的loader,上面我们给css文件匹配了两个用于处理层叠样式表的loader。

loader是链式调用的,第一个loader将结果传递给第二个loader,所以应保证style-loadercss-loader前。

创建src/hello.css

.hello {
    color: red;
}

修改index.js

import _ from 'lodash';
import './hello.css'


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

  element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  // [+] 为元素添加类
  element.classList.add('hello');
  return element;
}

document.body.appendChild(component());

引入文件

css是被打包进去了,css中通过url引入的文件呢?html中引入的文件呢?通过import引入的其他类型文件呢?

module.rules中添加下面代码

{
    test: /\.(png|svg|jpg|jpeg|gif)$/i,
    type: 'asset/resource'
}

现在遇到这些类型的文件时,webpack会使用内置的Assets Modules进行处理。当通过import img from './img.jpg'引入时,文件会被添加到output目录,并将img变量就是最终处理后的url,当css-loader处理url('./img.jpg')时,也会做类似的处理,处理后的文件路径会替换之前的路径,html-loader同样也会对通过src引用的文件做同样的处理。

cp /mnt/c/Users/lilpig/Pictures/猪.png ./src/pig.png
import Icon from './pig.png'

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

  // ...

  const icon = new Image();
  icon.src = Icon;

  element.appendChild(icon);

  return element;
}

对于字体文件,也是一样的,webpack的Assets Module支持处理任何类型的文件

 {
    test: /\.(woff|woff2|eot|ttf|otf)$/i,
    type: 'asset/resource'
}

其他结构化类型文件

最常用的结构化类型文件就是json了,webpack天生支持。

其它的还有CSV、TSV和XML等,可以安装csv-loaderxml-loader去支持这些文件。

npm i csv-loader xml-loader -S
{
    test: /\.(csv|tsv)$/i,
    type: 'csv-loader'
}, {
    test: /\.xml$/i,
    type: 'xml-loader'
}
import Table from './table.csv'
import Xml from './test.xml'

function component() {
  console.log(Table);
  console.log(Xml);
  // ...
}
// ...

自定义parser

parser可以将任意类型的文件转化成json,这里演示yaml

npm install yamljs -D
// webpack.config.js
const yaml = require('yamljs');

// ...
{
    test: /\.yml$/i,
    type: 'json',
    parser: {
        parse: yaml.parse
    }
}

回退代码

将代码恢复到管理资源前的状态

管理输出

创建print.js

export default function printMe() {
    console.log('I get called from print.js');
}

修改index.js

import _ from 'lodash';
import printMe from './print.js';

function component() {
  const element = document.createElement('div');
  const btn = document.createElement('button');

  element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  btn.innerHTML = 'Click me and check the console! ';

  btn.onclick = printMe;

  element.appendChild(btn);
  return element;
}

document.body.appendChild(component());

现在,webpack能很好的发现index.js引入了print.js,自动满足两者的依赖关系。但是最终的文件还是打包到了dist/bundle.js中。

我们想分离这两个文件,只需要在webpack.config.js中配置多个入口并且生成多个出口即可。

const path = require('path');

module.exports = {
    entry: {
        index: './src/index.js',
        print: './src/print.js'
    },
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'dist')
    },
}
➜  webpack-concept git:(master) ✗ tree dist
dist
├── index.bundle.js
├── index.bundle.js.LICENSE.txt
├── index.html
└── print.bundle.js

0 directories, 4 files

但是现在index.html中该引用哪个文件?它两个都要引用,我们还得修改它,难道每添加一个输出文件就要修改一次它吗?如果后期我们项目做大了还要在输出文件名中添加标识版本的hash值,那修改就会更频繁。

设置HtmlWebpackPlugin

npm i html-webpack-plugin -D
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: {
        index: './src/index.js',
        print: './src/print.js'
    },
    plugins: [
        new HtmlWebpackPlugin({
            title: '管理输出'
        })
    ],
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'dist')
    },
}

HtmlWebpackPlugin作为webpack插件存在,它会在打包后在dist文件夹中生成一个index.html并将所有bundle都添加进去。

清理dist文件夹

现在dist文件夹已经相当杂乱了,我们可以配置output.clean在每次打包时清理dist文件夹,这样每次就只会有最新生成的数据。

output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
    clean: true
},

开发环境

// webpack.config.js
module.exports = {
    mode: 'development',
    // ...
}

使用Source Map

将文件打包成bundle的坏处就是编译后的代码和编译前代码不一致,浏览器中运行出错很难找到问题所在,如果将多个文件打包成一个bundle,问题会变得更复杂。

使用source map可以把编译后的代码映射成原始代码

webpack.config.js中添加如下代码

devtool: 'inline-source-map',

现在错误代码会被映射到原始代码上,高科技嘿。

热重载

可以通过以下途径实现热重载

  1. webpack watch mode
  2. webpack-dev-server
  3. webpack-dev-middleware

webpack watch mode

webpack自带的功能,监测依赖图中所有文件,其中的某一个文件被更新,代码被重新编译。

可以在package.json中添加一个脚本,使用webpack --watch来启动。

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

不足是当代码重新编译后,还要刷新浏览器,也可以使用vscode的live server,但是webpack-dev-server已经提供了这个功能。

webpack-dev-server

npm i webpack-dev-server -D

webpack.config.js中写入

devServer: {
    static: './dist'
},

package.json中写入

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "watch": "webpack --watch",
    "start": "webpack serve --open"
},

然后使用npm run start

代码分离

最简单的代码分离之前已经做过了,通过编写多个入口点来进行分离。

entry: {
    index: './src/index.js',
    print: './src/print.js'
},
output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
    clean: true
},

这样会有个问题,假如我在print.js中也引入lodash,那么lodash会分别被注入到两个bundle中,这样前端相当于耗费无用流量。

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

export default function printMe() {
    console.log('I get called from print.js');
    console.log(_.join(['Another', 'module', 'loaded'], ' '))
}

注意看这里的大小

我们可以把lodash单打个bundle,然后让printindex依赖这个。

entry: {
    index: {
        import: './src/index.js',
        dependOn: 'lodash'
    },
    print: {
        import: './src/print.js',
        dependOn: 'lodash'
    },
    lodash: 'lodash'
},

如果在一个HTML页面上使用多个入口时,还要设置如下内容

optimization: {
    runtimeChunk: 'single',
},

会生成一个runtime.bundle.js,目前还不知道作用是啥。

SplitChunksPlugin

SplitChunksPlugin插件将公用的依赖自动提取到已有的入口chunk中,或者提取到一个新的chunk。(一个编译出来的文件就叫一个chunk)

也就是说我们不再需要手动提取公用依赖,如果依赖多了,手动提取也很麻烦

entry: {
    index: './src/index.js',
    print: './src/print.js'
},
output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
    clean: true
},
// [+]
optimization: {
    splitChunks: {
        chunks: 'all'
    }
}

动态导入

可以通过import(xx).then()进行动态导入

缓存

恢复项目

只留一个index.js,内容如下

import _ from 'lodash';
function component() {
  const element = document.createElement('div');

  element.innerHTML = _.join(['Hello', 'webpack'], ' ');

  return element;
}

document.body.appendChild(component());

webpack.config.js

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

module.exports = {
    entry: './src/index.js',
    plugins: [
        new HtmlWebpackPlugin({
            title: 'Caching'
        })
    ],
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist'),
        clean: true
    },
}

修改输出文件名

浏览器的缓存能够给客户端和服务器端节省流量和加载时间,但问题是当服务器端的代码更新了,无法及时反应到客户端。

可以通过将输出文件名中添加[contenthash],每次重新编译时都会计算文件的hash,这样代码更新后模块的文件名就会更新,相应的缓存也会更新。

output: {
    filename: '[name].[contenthash].js',
    path: path.resolve(__dirname, 'dist'),
    clean: true
},

提取引导模板

SplitChunksPlugin的目的是将模块分离到单独的bundle,对于optimization.runtimeChunk选项则是webpack提供的一项优化,用于将运行时代码分离。

将第三方模块打包到单独的名为vendor的chunk中,因为它们基本不会被更改,它们的contenthash不会更改,所以客户端可以利用缓存来减少网络流量。

下面将第三方模块全部打包到vendor-[contenthash].bundle.js

optimization: {
    runtimeChunk: 'single',
    splitChunks: {
        cacheGroups: {
            vendor: {
                test: /[\\/]node_modules[\\/]/,
                name: 'vendors',
                chunks: 'all'
            }
        }
    }
}

这下第三方库被打包到vendors中,运行时代码也被打包到runtime中,main的体积减小。

模块标识符

好像是默认情况下,你重新在index.js中引入其他依赖,vendorruntime的hash都会更新,然后这时候只需要在optimization中设置即可修复

moduleIds: 'deterministic'

但是我不设也不会更新其它的hash。

原因应该是计算hash时,模块的id也被考虑了,默认情况下id会随着模块解析顺序更改,引入新的依赖会打破这个顺序,所以vendor的hash也更新了。

创建Library

创建Library

如果想把自己的代码作为一个library模块打包供其他人引用,webpack也提供了相应功能。

mkdir webpack-number
cd webpack-number
npm i webpack webpack-cli lodash -D

这里将lodash也作为devDependencies导入是因为不想让lodash也被打包到我们的库中,让库变得很大。我们需要用户自行导入这个库。

创建如下结构

webpack.config.js

const path = require('path');

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.join(__dirname, 'dist'),
        filename: 'webpack-number.js',
    }
}

src/ref.json

[
  {
    "num": 1,
    "word": "One"
  },
  {
    "num": 2,
    "word": "Two"
  },
  {
    "num": 3,
    "word": "Three"
  },
  {
    "num": 4,
    "word": "Four"
  },
  {
    "num": 5,
    "word": "Five"
  },
  {
    "num": 6,
    "word": "Six"
  },
  {
    "num": 7,
    "word": "Seven"
  },
  {
    "num": 8,
    "word": "Eight"
  },
  {
    "num": 9,
    "word": "Nine"
  },
  {
    "num": 0,
    "word": "Zero"
  }
]

src/index.js

import _ from 'lodash';
import numRef from './ref.json'

export function numToWord(num) {
    return _.reduce(
        numRef,
        (accum, ref) => {
            return ref.num === num ? ref.word : accum
        },
        ''
    );
}

export function wordToNum(word) {
    return _.reduce(
        numRef,
        (accum, ref) => {
            return ref.word.toLowerCase() === word.toLowerCase() ? ref.num : accum
        },
        0
    );
}

现在和打包一个普通app没区别,所以还需要修改webpack.config.js让webpack将我们的代码作为一个模块打包。

output: {
    path: path.join(__dirname, 'dist'),
    filename: 'webpack-number.js',
    // [+]
    library: 'webpackNumbers'
}

现在打包它,就可以按如下办法引用

<script src="./webpack-number.js"></script>
<script>
    let word = webpackNumbers.numToWord(9);
    let num = window.webpackNumbers.wordToNum('NINe');
    console.log(word);
    console.log(num);
</script>

作为一个库作者,当然不希望自己的库只能通过script标签引用了,我们希望它能兼容不同的环境,如CommonJS、AMD等。

const path = require('path');

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.join(__dirname, 'dist'),
        filename: 'webpack-number.js',
        // [+]
        library: {
            name: 'webpackNumbers',
            type: 'umd',
        },
    },

}

修改library并将type设置成umd,打包的代码兼容CommonJS、AMD和script标签。

如果在node中运行,可能还需要在output中添加

globalObject: 'this'

外部化依赖

现在项目依赖的lodash还是会被打包到最后的bundle中,我们需要使用externals来告诉webpack,这个依赖应该是外部来引入的

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.join(__dirname, 'dist'),
        filename: 'webpack-number.js',
        library: {
            name: 'webpackNumbers',
            type: 'umd'
        },
        globalObject: 'this',
    },
    externals: {
        lodash: {
            commonjs: 'lodash',
            commonjs2: 'lodash',
            amd: 'lodash',
            root: '_'
        }
    }
}

这样导出的包就不存在lodash的代码了。

外部化的限制

如果要调用多个外部依赖,externals无法直接排除全部,而是要逐个排除,或者使用正则。

externals: [
    'library/one',
    'library/two',
    // 匹配以 "library/" 开始的所有依赖
    /^library\/.+$/,
],

所以一个好的实践应该是将所有依赖放到一个文件夹。

环境变量

Webpack有一个自己的环境变量,不是系统环境变量。可以通过--env来设置。

npx webpack --env goal=local --env production --progress

--env production代表将env.production设置为true。

可以通过将我们的webpack.config.js导出的东西从对象转换成函数,并注入env参数,来获取环境变量。

const path = require('path');

module.exports = (env) => {
    console.log(env.goal);
    return {
        entry: './src/index.js',
        output: {
            path: path.join(__dirname, 'dist'),
            filename: 'bundle.js',
        },
    }
}
posted @ 2021-10-15 09:12  yudoge  阅读(112)  评论(0编辑  收藏  举报