webpack整合笔记初探

211 阅读9分钟

一 何为webpack

01 环境搭建

安装nvm curl -o- raw.githubusercontent.com/creationix/… | bash 把nvm填加到环境变量 source ~/.bash_profile nvm的常用命令 :(反正我也记不住)

  • nvm install stable ## 安装最新稳定版 node
  • nvm install ## 安装指定版本
  • nvm uninstall ## 删除已安装的指定版本,语法与install类似
  • nvm use ## 切换使用指定的版本node
  • nvm ls ## 列出所有安装的版本
  • nvm ls-remote ## 列出所有远程服务器的版本(官方node version list)
  • nvm current ## 显示当前的版本
  • nvm alias ## 给不同的版本号添加别名
  • nvm unalias ## 删除已定义的别名
  • nvm reinstall-packages ## 在当前版本 node 环境下,重新全局安装指定版本号的 npm 包
  • nvm 所有命令 自行logo

何为 webpack

其最核心的功能是解决模块之间的依赖。 在以前引入多个script文件到页面中,有很多缺点:

  • 需要手动维护js的加载顺序
  • 每个script标签都是一次请求
  • 在每个script中,顶层作用域即全局作用域,会造成全局作用域污染 模块化解决了上述所有问题。
  • 通过导入和导出语句可清晰看到模块的依赖关系
  • 模块可借助工具打包,合并资源文件,减少了网络开销
  • 多个模块之间是作用域隔离的,彼此不会有命名冲突

02 以node规范创建项目

npm init -y package.json

{
    "private": true,  // 私有项目
    // ...
}

工程源代码放在/src中,输出资源放在/dist中。

在项目目录中直接运行webpack -v是无法找到webpack的 运行npx webpack -v会找到项目目录中node_modules里的webpack npx webpack --config wepback.dev.js

npm i --save-dev是将npm包作为(devDependencies)开发环境依赖。假如上线时要进行依赖安装,可通过npm i --produciton过滤掉devDependencies中的冗余模块,从而加快安装和发布的速度。

03 Mode

Mode用来指定当前的构建环境是: production、development 还是 none 设置mode可使用webpack内置的函数,默认值为production

Mode内置函数功能

选项 描述
development 设置process.env.NODE_ENV 的值为 development . 开启NamedChunksPlugin 和 NamedModulesPlugin
production 设置process.env.NODE_ENV 的值为 production .开启FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin 和 TerserPlugin
none 不开启任何优化选项

04 performance 性能相关错误

{ performance: false // 不提示性能相关错误 }

二 模块打包

模块之于程序,细胞之于生物体

01 commonJS

var module = {...}
module.exports = {...}

commonJS也支持简化的导出方式

exports.name = 'calculater'
exports.add = function(a, b){
    return a + b;
}

exports.add相当于在module.exports的简写。不要直接给exports赋值,否则会导致其失效。

exports = {
    name: 'calculater'   // 失效
}

02 ES6 Module

在ES6 Module中不管开头是否有 use strict 都会采用严格模式 在ES6 Module 中使用 export命令来导出模块

导出

  • 命名导出
export const name = "calculater"
export const add = function(a, b){return a + b}

const name = "calculater"
const add = function(a, b){return a + b}
export {name, add}
export {name, add as getSum}  // 通过as关键字对变量重命名, 导入时即为 name 和 getSum
  • 默认导出
export default {
    name: "calculater", 
    add: function(a, b){return a + b}
    
}

导入

可通过 as 对导入的变量重命名

import {name, add as caculateSum } from './calculator.js'

导入多个变量时可通过 * as 整体导入。

    import * as calculator from './calculator.js'

一个混合导入方式

import React, {Component} from 'react'

03 CommonJS 和 ES6 Module区别

动态与静态

CommonJS是动态的,ES6 Module是静态的。这里的“动态”含义是,模块依赖关系的建立发生在代码运行阶段:而“静态”则是模块依赖关系的建立发生在代码编译阶段。 ES6的编译阶段可分析出模块的依赖关系。相比CommonJS具备以下优势:

  • 死代码检测和排除
  • 模块变量类型检查
  • 编译器优化

值拷贝与动态映射

在导入一个模块时,CommonJS是导出值的拷贝:而在ES6 Module中则是值的动态映射且只读。 CommonJS的值拷贝 calculator.js

var count = 0;
module.exports = {
    count: count,
    add: function(a,b){count += 1; return a + b}
}

index.js

var count = require('calculator.js')
var add = require('calculator.js')
console.log(count);  // 0
add(2,3)
console.log(count);  // 0  calculator.js中变量的改变不会对这里的拷贝值造成影响
count += 1;
console.log(count);  // 1 拷贝值可变

ES6 Module的映射 calculator.js

let count = 0;
conts add = function(a,b){count += 1; return a + b}
export {count, add}

index.js

import {count, add} from './calculator.js'
console.log(count); // 0
add(2,3)
console.log(count);  // 1 实时反映calculator.js中count值的变化
count += 1; //不可更改,会抛出SyntaxError:"count" is read-only

三 资源输入输出

webpack会从入口文件刊检索,并将具有依赖关系的模块生成一棵依赖树,最终得到一个chunk。由这个chunk得到的打包产物我们一般称为bundle。

01 context

context可理解为资源入口的路径前缀,在配置时要求必须绝对路径。

module.exports = {
    context: path.join(__dirname, './src'),
    entry: './scripts/index.js'
}
module.exports = {
    context: path.join(__dirname, './src/scripts'),
    entry: './index.js'
}

配置context主要目的是让entry简洁,尤其在多入口时。可省略,默认为当前工程根目录

02 Entry

entry可多种形式:字符串、数组、对象、函数

数组会将多个资源预先合并,在打包时会将数组中最后一个元素作为实际入口

module.exports = {
    entry: ['babel-polyfill', './src/index.js']
}

上面等价于

// webpack.config.js
module.exports = {
    entry: './src/index.js'
}
//index.js
import 'babel-polyfill'

对象类型入口

多入口必须使用对象形式

module.exports = {
    entry: {
        index: './src/index.js',
        lib: './src/lib.js',
        admin: ['babel-polyfill', './src/index.js']
    }
}

在使用字符串或数组定义单入口时,并没有办法更改chunk name,只能为默认的"main"

函数类型入口

只要返回上面介绍的任何配置即可,优点在于可在函数体里添加动态逻辑也可返回一个promise

module.exports = {
    entry: ()=> new Promise((resolve)=>{
        setTimeout(()=>{
            resolve('./src/index.js')
        }, 1000)
    })
}

03 实例

单页应用(SPA)一般定义单一入口即可

module.exports = {
    entry: './src/app.js'
}

这样的好处是只会产生一个JS文件,依赖关系清晰。弊端是所有模块都打包到一起,大型项目体积过大,降底了页面渲染速度。

在webpack默认配置中,当一个bundle大于250kb时(压缩前)会认为bundle过大,在打包时会发出警告。

多页面打包基本思路

每个页面对应一个entry,一个html-webpack-plugin 缺点:每次新增或删除页面需要改webpack配置

解决方案:动态获取entry和设置html-webpack-plugin数量

webpack.config.js

const makePlugins = (configs) => {
    const plugins = [
        //...
    ]
    Object.keys(config.entry).forEach((item)=>{
        plugins.push(
            new HtmlWebpackPlugin({
                template: path.join(__dirname, `src/${pageName}/index.html`),
                filename: `${item}.html`,
                chunks: ['runtime', 'vendors', `${item}`]  // 需要的chunks
            })
        )
    })
    return plugins;
}
const configs = {
    entry: {
        index: './src/index.js',
        admin: './src/admin.js',
        lib: './src/lib.js'
    }
}
configs.plugins = makePlugins(configs)

03 Output

filename

  1. 输出资源的文件名
  2. 还可以是一个相对路径,即使路径目录不存在也没关系,webpack会创建该目录
  3. 多入口场景中,需要为对应产生的每个bundle指定不同的名字
module.exports = {
    output: {
        filename: 'bundle.js',          // 1.
        filename: './js/bundle.js',     // 2.
        filename: '[name].js'           // 3.
    }
}

path 可指定资源输出的位置,要求必须为绝对路径

module.exports = {
    output: {
        path: path.join(__dirname, 'dist')
    }
}

webpack4后,output.path默认为dist目录,可不配置

publicPath

同path不同,path用来指定资源的输出位置,publicPath用来指定资源的请求位置。

  • 输出位置:打包完成后资源产生的目录,一般为dist
  • 请求位置:由JS或CSS所请求的间接资源路径。如异步加载的JS,从CSS请求图片字体等。publicPath就是指定成部分间接资源的请求位置。 publicPath有三种形式
  1. HTML相关: 可将publicPath指定为HTML的相对路径,在请求这此资源时会以从前页面HTML所有路径加上相对路径,构成实际请求的URL
// 假设当前HTML地址为:https://abc.com/app/index.html
// 异步加载资源名为0.chunk.js
publicPath: ""              // https://abc.com/app/0.chunk.js
publicPath: "./js"          // https://abc.com/app/js/0.chunk.js
publicPath: "../assets/"    // https://abc.com/assets/0.chunk.js
  1. Host相关: 若publicPath以'/'开始,则代表以当前页面的host name为基础路径
// 假设当前HTML地址为:https://abc.com/app/index.html
// 异步加载资源名为0.chunk.js
publicPath: "/"              // https://abc.com/0.chunk.js
publicPath: "/js"            // https://abc.com/js/0.chunk.js
publicPath: "/dist/"         // https://abc.com/dist/0.chunk.js
  1. CDN相关:以协议头或相对协议形式开始
// 假设当前HTML地址为:https://abc.com/app/index.html
// 异步加载资源名为0.chunk.js
publicPath: "http://cdn.com"           // http://cdn.com/0.chunk.js
publicPath: "https://cdn.com"          // https://cdn.com/0.chunk.js
publicPath: "//cdn.com/assets/"        // cdn.com/assets/0.chunk.js

WebpackDevServer中也有一个publicPath,这个publicPath与webpack中的配置含义不同,它的作用是指定WebpackDevServer的静态资源服务路径。 为了避免开发环境和生产环境产生不一致的疑惑,可将WebpackDevServer中的publicPath 与 webpack中的output.path保持一致。

module.exports = {
    output: {
        path: path.join(__dirname, 'dist')
    },
    devServer: {
        publicPath: '/dist/'
    }
}

04 文件指纹如何生成

  • Hash: 和整个项目的构建相关,只要项目文件有修改,整个项目构建的hash值就会更改(A页面修改,会影响B页面的hash)
  • Chunkhash: 和webpack打包的chunk有关,不同的entry会生成不同的chunkhash值(A页面修改,不会影响B页面/A页面修改JS,会影响A页面CSS的hash)
  • Contenthash: 根据文件内容来定义hash,文件内容不变,则contenthash不变(A页面修改JS,不会修改A页面的CSS)

JS的文件指纹设置

设置output的filename, 使用[chunkhash]

module.exports={
    output:{
        filename:'[name][chunkhash:8].js',  //对于JS使用chunkhas
    },
    plugins:[
        new MiniCssExtractPlugin({                 // 提取css到一个独立的文件
            filename:'[name][countenthast:8].css'  // 对于css使用countenthash
        })
    ],
    module:{
        rules:[
            {
                test:/\.(png|svg|jpg|gif)$/,
                use:[
                    {
                        loader: 'file-loader',
                        options:{
                            name: 'img/[name][hash:8].[ext]'   //此处hash和js里的hash不同
                        }
                    }
                ]
            }
        ]
    }
}
占位符 含义
[ext] 资源后缀名
[name] 文件名称
[path] 文件的相对路径
[folder] 文件所在的文件夹
[contenthash] 文件的内容hash,默认是md5生成
[hash] 文件内容的hash,默认是md5生成
[emoji] 一个随机的指代文件内容的emoj

四 Loaders

webpack开箱即用只支持JS和JSON两种文件类型,通过Loaders去支持其它文件类型并且把它们转化成有效的模块,并且可添加到依赖图中。 Loaders本身是一个函数,接受源文件作为参数,返回转换的结果。

基本配置项

  • test 可接收一个正则或一个元素为正则的数组
  • use可接收一个数组,数组包含该规则所使用的loader
  • exclue与include是用来排除或包含指定目录下的模块,可接收正则或字符串(文件绝对路径),以及由它们组成的数组。 两者同时存在时exclude优先级更高,正由于此,可对include中的子目录进行排除
rules: [
    {
        exclude: /src/lib/,  //排除src中的lib目录
        include: 'src'
    }
]
  • resoure与issuer,可用于更加精确地确定模块规则的作用范围 在webpack中,被加载模块是resource,加载者是issuer 前面介绍的test/exclude/include本质上属于对resource也就是被加载者的配置,想要对加载者配置使用issuer
rules: [
    {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
        exclude: /node_modules/,
        issuer: {
            test: \.js$/,
            include: /src/pages/
        }
    }
]

上面等价于

rules: [
    {
        use: ['style-loader', 'css-loader'],
        resource: {
            test: /\.css$/,
            exclude: /node_modules/,
        },
        issuer: {
            test: \.js$/,
            include: /src/pages/
        }
    }
]
  • enforce 用来指定一个loader的种类,只接收 "pre" 或 "post"

常见Loaders


名称 描述
babel-loader 转换ES6、7等JS新特性语法
css-loader 支持.css文件的加载和解析
less-loader 将less文件转换成css
ts-loader 将TS转换为js
file-loader 进行图片、字体等的打包
raw-loader 将文件以字符串的形式导入
thread-loader 多进程打包JS和CSS

01 babel

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

  • babel-loader 是使用babel与webpack协同工作的模块
  • babel/core 是babel的核心模块
  • babel/preset-env 根据用户设置的目标环境自动添加所需的插件和补丁来编译es6代码
reles: [
    {
        test:/\.js$/,
        exclude: /node_modules/,        // 必配项忽略node_modules目录
        use: {
            loader: 'babel-loader',
            options: {
                cacheDirectory: true,       // 启用缓存,优化打包速度
                presets: [[
                    'env', {
                        modules: false // 禁用模块语句的转化(见下面说明)
                        
                    }
                ]]
            }
        }
    }
]

babel/berset-env会将ES6 module转化为CommonJS形式,这会导致tree-shaking失效 将babel/presets-env设置为false会禁用模块语句的转化,将ES6 module语法交给webpack处理

module.exports = {
    rules: [{
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader",
        options:{
            presets: [["@babel/preset-env", {
                useBuiltIns: 'usage', // 只引入用到的polyfill
                targets: {
                    chrome: '67'
                }
            }]] 
        }
    }]
}

npm i -S @babel/polyfill 在所有代码运行前要 import '@babel/polyfill', 但是polyfill可优化CDN引入

打包UI库时,配置不同,import '@babel/polyfill' 会污染全局变量,需删除,然后如下配置 npm i -D @babel/plugin-transform-runtime npm i -S @bable/runtime @babel/runtime-corejs2

module.exports = {
    rules: [{
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader",
        options:{
            "plugins": [
                [
                    "@babel/plugin-transform-runtime",
                    {
                        "corejs": 2,
                        "helpers": true,
                        "regenerator": true,
                        "useESModules": false
                    }
                ]
            ]
        }
    }]
}

babel 配置会很长,可创建.babelrc

{
    "plugins": [
        [
            "@babel/plugin-transform-runtime",
            {
                "corejs": 2,   // 只引入用到的polyfill
                "helpers": true,
                "regenerator": true,
                "useESModules": false
            }
        ]
    ]
}

babel 打包 React

npm i -S @babel/preset-react

.babelrc

{
    presets: [
        [
            "@babel/preset-env", {
                useBuiltIns: 'usage', // 只引入用到的polyfill
                targets: {
                    chrome: '67',
                }
            }
            ],
            "@babel/preset-react"
    ] 
}

02 TypeScript 配置

npm i -S @types/lodash (在TS中使用lodash) import * as _ from 'lodash'

webpack.config.jsx

module.exports = {
    mode: 'production',
    entry: './src/index.tsx',
    module: {
        rules: [{
            test: /\.tsx?$/,
            use: 'ts-loader',
            exclude: /node_modules/
        }]
    },
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    }
}

tsconfig.json

{
    "compilerOptions":{
        "outDir": "./dist",
        "module": "es6",
        "target": "es5",
        "allowJs": true
    }
}

03 html-loader

将HTML文件转化为字符串并进行格式化,这使得我们可把一个HTML片段通过js加载进来 npm i html-loader

{
    test: /\.html$/,
    use: 'html-loader'
}

五静态资源

resolve.alias创建的alias不仅可在JS中使用,在html和css中也可使用

resolve:{
    alias: {
        '@assets':path.resolve(__dirname, './src/assets')
    }
}
.bg-img{ background: url(~@assets/img/small.png) no-repeat}
<img src="~@assets/img/foo/bar.png"/>

Tips: HTML使用<img>引入图片等静态资源时,需要添加html-loader配置,不然不会处理静态资源路径问题。

01 file-loader

用于打包文件类型的资源,并返回其publicPath

file-loader引入图片

import avatar from './avatar.png'
var img = new Image()
img.src = avatar;

var root = document.getElementById('root')
root.append(img)

webpack.config.js

module.exports = {
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist'),
        publicPath: './assets/'
        
    },
    module: {
        rules: [
            {
                test: /\.(jpg|png|gif)$/,
                use: {
                    loader: 'file-loader',
                    options:{
                        name:'[name]_[hash].[ext]',
                        outputPath: './another-path/',   
                        // 打包到dist目录下哪里会覆盖output.publicPath
                    }
                }
            }
        ]
    }
}

output.path是资源的打包输出路径,output.publicPath是资源引入路径。

02 url-loader

与file-loader类似,不同在于可设置一个文件大小的阀值,大于该阀值时与file-loader一样返回publicPath,小于时返回文件base64形式编码。

module.exports = {
    module: {
        rules: [
            {
                test: /\.(jpg|png|gif)$/,
                use: {
                    loader: 'url-loader',
                    options:{
                        name:'[name]_[hash].[ext]',
                        outputPath: 'images/',
                        limit: 2048   // 图片小于2048kb以base64插入到代码里,否同file-loader
                    }
                }
            }
        ]
    }
}

六 插件

插件用于bundle文件的优化,资源管理和环境变量注入 作用于整个构建过程,增强webpack

名称 描述
CommonsChunkPlugin 将chunks相同的模块代码提取成公共JS
CleanWebpackPlugin 清理构建目录
ExtractTextWebpackPlugin 将CSS从bunlde文件里提取厂一个独立的css文件
CopyWebpackPlugin 将文件或文件夹考贝到构建的输出目录
HtmlWebpackPlugin 创建html文件去承载输出的bundle
UglifyisWebpackPlugin 压缩js
ZipWebpackPlugin 将打包出的资源生成一个zip包

01 html-webpack-plugin

会在打包结束后在dist生成html文件并把bondle.js引入到html中 npm i html-webpack-plugin -D

const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
    plugins: [
        new HtmlWebpackPlugin({
            template: path.join(__dirname, 'src/search.html'),
            filename:'search.html',
            chunks:['search'],
            inject:true,
            minify:{                            // 压缩配置
                html5: true,
                collapseWhitespace: true,
                preserveLineBreaks: false,
                minifyCSS: true,
                minifyJS: true,
                removeComments: false
            }
        })
    ]
}

02 clean-webpack-plugin

清除dist目录

const CleanWebpackPlugin = require('clean-webpack-plugin')
module.exports = {
    plugins: [
        new CleanWebpackPlugin(['dist'])
    ]
}