前端进阶之Webpack(一)
前言
不知不觉,工作两年了。
最近,参与搭建了一个微应用项目,由于使用的内部框架并不太成熟,而且是第一次参与构建大型项目,各种各样的坑,踩了个遍。
回想刚入行时,用vue-cli快速搭建项目,感觉搭项目也没啥难的,可谓是初生牛犊,不怕虎。
项目慢慢过渡到了迭代阶段,汇总一下热乎的知识,博客写了,就等于我没忘。
什么是Webpack?
在搭建项目的过程中,很多问题都可以总结成一个问题——兼容性问题。
剩下的问题,虽然五花八门,但始终围绕着“便捷”二字。
从作用上理解,Webpack是用来方便程序员开发,以及解决兼容性问题的工具。
从代码本质上理解,Webpack和其他依赖包没有区别,同样通过一个npm仓库进行管理,内部是nodejs语法。
不能少的node知识储备
node基础不错的朋友可以跳过这一节,但如果对node比较陌生的话,我建议还是看看,使用Webpack的过程中会用到。
模块化
一个依赖包就是一个模块,最基础的模块只需要两样东西:
- package.json:模块描述文件。
- [name].js:模块入口文件(模块的功能会被通过module.exports导出)。

 上传模块的步骤:
上传模块的步骤:
- 注册仓库账号,没有公司仓库的话,可以直接去官网www.npmjs.com/注册一个。
- npm adduser:登录账号。
- npm publish:上传模块至对应的仓库。

上传之后,模块就和我们常见的依赖包一样可以被下载。

下载后,通过require引入模块就可以使用内部功能,我在demo中只是简单抛出了一个对象{a: '111'},我们常用的依赖包功能自然会复杂的多。

Webpack多了不少文件,但本质和我的demo是一样的,是node编写的包,都属于用户编写的模块,而http、fs、path这些属于核心模块。
两者有什么区别?下面接着会讲。
模块引入
在node中引入模块,会经历三个步骤:
- 路径分析
- 文件定位
- 编译执行
一个用户编写的模块,通过require('node_test_demo_sunshaohe')引入项目,没有加任何路径和文件后缀,node会先认为它是一个node自带的核心模块,找不到的话,会一层一层向外循环遍历寻找同名的.js文件,还是找不到的话,会去寻找同名的.json文件,最后寻找同名的.node文件。
因此,外部引入一个.node或.json文件,最好写清楚路径和文件后缀。
核心模块在node源代码编译的过程中,就被编译成了二进制执行文件,而且在路径分析中会被优先判断,所以它的加载速度是最快的。
全局对象
由于自定义全局变量会造成环境污染,因此,需要尽可能利用node自带的全局对象。
__filename
__filename表示正在执行脚本的文件名(project/src/main.js),并附带绝对路径,如果在模块中,附带的是模块的路径(module/app.js)。
__dirname
__dirname表示执行脚本目录的绝对路径
process
在使用本地指令的时候,一般会用到process。
由于我做的是多语言项目,某些国家会有专门的定制版本,在启动指令中用不同的COUNTRY值来区分,在项目中可以用process.env.COUNTRY获取环境变量。
process.env一次只能获取一个环境变量,在想要一次获取多个环境变量的时候,可以使用process.argv,它会以数组的形式,返回所有环境变量。
除此之外,process还有许多其他的属性和方法,感兴趣的朋友可以自行了解。
核心模块
path
path是最常用的核心模块,用于处理文件路径。
path.join():用于连接路径,正确使用相应系统的分隔符,Unix是\,Windows是/。
eg:path.join('a', 'b', 'c')等价于a/b/c
path.resolve():用于解析路径,从右往左分析,最终得到一个绝对路径。
eg:path.resolve('/a/b', './c/d'),解析后得到/a/b/c/d
eg:path.resolve('/a/b', '/c/d'),解析后得到/c/d
eg:path.resolve('/a/b', '../c/d'),解析后得到/a/c/d
Webpack的打包流程
- 读取Webpack的配置参数。
- 创建Compiler对象并开始解析项目。
- 从入口开始解析,找到其依赖的模块,递归遍历分析,形成依赖关系树。
- 对不同文件类型的模块,使用对应的loader进行编译,最终转换成JavaScript。
- 根据入口和依赖关系,输出成果物。
- 整个打包过程会发布事件,触发订阅了对应事件的plugin。
配置参数包括webpack.config.js里的固定配置和指令中的变量,Webpack会整合后,传给Compiler。
以下是一个简易的Compliler类:
class Compiler(options) {
    constructor(options) {
        const {entry, output} = options; //options是配置参数
        this.entry = entry;
        this.output = output;
        this.modules = [];
    }
    
    run(){} //Webpack打包
    generate() {} //输出成果物
}
Webpack配置
Webpack默认会从webpack.config.js中读取配置参数,配置参数决定了Webpack会如何去打包,以及输出一个什么样的成果物。
context
基础目录context是一个绝对路径,Webpack解析相对路径时,会以context作为参考。
const path = require('path');
moduile.exports = {
    context: path.resolve(__dirname, 'app')
}
entry
Webpack从入口entry开始打包,entry支持三种数据类型:
- String:entry: './main.js'
- Array:entry: ['./main.js', './app.js']
- Object:entry: {a: './main.js', b: './app.js'}
output
output配置影响成果物如何输出。
filename
output.filename配置输出文件的名称,如果只有一个输出文件,可以写成:
module.exports = {
    output: {
        filename: 'bundle.js'
    }
}
有多个入口文件,需要输出多个文件时:
module.exports = {
    output: {
        filename: '[id][name][hash][chunkhash].js'
    }
}
- id:- Chunk的唯一标识,从0开始。
- name:- Chuunk的名称,由入口文件的名称决定。
- hash:- Chunk唯一标识的- hash值。
- chunkhash:- Chunk内容的- hash值。
hash和chunkhash的长度是可指定的,[hash: 8]代表取8位的hash值,默认是20位。
chunkFilename
output.chunkFilename配置无入口的Chunk在输出时的文件名称。

我用红框框住的就是无入口的Chunk,app.js则是入口文件的Chunk。
Webpack默认会把异步加载的组件提取出来,脱离入口依赖图,单独打包成一个Chunk。我们还可以根据需要制定拆分规则,具体配置后续会讲。
path
output.path配置输出文件的目录,必须是绝对路径。
const path = require('path');
module.exports = {
    output: {
        path: 'path.resolve(__dirname, 'dist')'
    }
}
publicPath
output.publicPath配置线上资源的URL前缀。
const path = require('path');
module.exports = {
    output: {
        pubilicPath: 'http://example.com/assets/'
    }
}
在项目中可以通过http://example.com/assets/image.png获取资源。
library和libraryTarget
当用Webpack去构建一个可以被其他项目导入使用的库时,就会用到library和libraryTarget。
简而言之,在一个项目中引用另一个项目的成果物。
ouput.library配置导出库的名称。
output.libraryTarget配置以何种方式导出库,共有七种:
- umd:万能库,下面的每种库都有对应的使用方法,所有方法都可以用来调用- umd库,不知道用什么的时候,用- umd就对了。- umd的兼容性自然是牺牲性能的,熟悉以后,做优化的时候,可以换成更合适的库。
- var:编写的库将通过- var被赋值给通过- library指定名称的变量。
module.exports = {
    output: {
        library: 'libraryName',
        libraryTarget: 'var'
    }
}
打包好的成果物,在另一个项目中使用:
// Webpack输出的代码
var LibraryName = lb_code;
// 使用库的方法
LibraryName.doSomething();
- commonjs:编写的库将通过- CommonJS规范导出。- node是按照- CommonJS规范编写的。
module.exports = {
    output: {
        library: 'libraryName',
        libraryTarget: 'commonjs'
    }
}
打包好的成果物,在另一个项目中使用:
// Webpack输出的代码
exports['LibraryName'] = lib_code;
// 使用库的方法
require('library-name-in-npm')['LibraryName'].doSomething();
- commonjs2:编写的库将通过- CommonJS2规范导出,与- commonjs类似,在其基础上,可以通过- module.exports的导出方式。
module.exports = {
    output: {
        library: 'libraryName',
        libraryTarget: 'commonjs2'
    }
}
打包好的成果物,在另一个项目中使用:
// Webpack输出的代码
module.exports = lib_code;
// 使用库的方法
require('library-name-in-npm')['LibraryName'].doSomething();
- this:编写的库将通过- this被赋值给通过- library指定的名称。
module.exports = {
    output: {
        library: 'libraryName',
        libraryTarget: 'this'
    }
}
打包好的成果物,在另一个项目中使用:
// Webpack输出的代码
this['LibraryName'] = lib_code;
// 使用库的方法
this.LibraryName.doSomething();
- window:编写的库将通过- window被赋值给通过- library指定的名称。
module.exports = {
    output: {
        library: 'libraryName',
        libraryTarget: 'window'
    }
}
打包好的成果物,在另一个项目中使用:
// Webpack输出的代码
window['LibraryName'] = lib_code;
// 使用库的方法
window.LibraryName.doSomething();
- global:编写的库将通过- global被赋值给通过- library指定的名称。
module.exports = {
    output: {
        library: 'libraryName',
        libraryTarget: 'global'
    }
}
打包好的成果物,在另一个项目中使用:
// Webpack输出的代码
global['LibraryName'] = lib_code;
// 使用库的方法
global.LibraryName.doSomething();
optimization
optimization用于对打包结果进行优化,之前提到的拆包规则便是在这里配置。
minimizer
optimization.minimizer是一个数组,会执行用于代码压缩的插件。
const TerserPlugin = require('teser-webpack-plugin');
module.exports = {
    optimization: {
        minimizer: [
            new TerserPlugin({
                // 开启多进程进行打包
                parallel: true;
                terserOptions: {
                    // 代码压缩
                    compress: {
                        // 打包时自动去掉console.log
                        drop_console: true,
                        // 打包时自动去掉debugger
                        drop_debugger: true
                    }
                }
            })
        ]
    }
}
splitChunks
optimization.splitChunks配置拆包规则。
module.exports = {
    optimization: {
        splitChunks: {
            // 规则的适用对象,all表示所有,async表示异步(默认),initial表示同步
            chunks: all,
            // Chunk大小超过30kb时,才进行代码拆分
            minSize: 30000,
            // 如果Chunk大小超过50kb,那么Webpack会尝试对其进行拆分
            maxSize: 50000,
            // 一个模块至少要被引用3次,才会被单独打包成一个Chunk
            minChunks: 3,
            // 如果Chunk的异步请求数超过5个,那么Webpack会尝试对其进行拆分
            maxAsyncRequests: 5,
            // 入口点能被拆分的最大数量
            maxInitialRequests: 1,
        }
    }
}
module
module配置处理模块的规则。
rules
module.rules是一个数组,读取Loader来解析对应类型的文件。
module.exports = {
    module: {
        rules: [
            {
                // 命中JavaScript文件
                test: /\.js$/,
                // 用babel-loader转换JavaScript文件
                use: ['babel-loader'],
                // 匹配范围是src下的文件
                include: path.resolve(__dirname, 'src'),
                // Loader的配置项
                options: {
                    preset: ['es2015']
                }
            },
            {
                test: /\.scss$/,
                // 使用一组Loader去处理scss文件
                // 会先执行sass-loader,再执行css-loader,最后执行style-loader
                // Webpack会按照配置从下到上,从右到左处理文件
                use: ['style-loader', 'css-loader', 'sass-loader'],
                // 搜索范围跳过node_modules下的文件
                exclude: path.resolve(__dirname, 'node_modules')
            }
        ]
    }
}
resolve
resolve配置Webpack如何寻找模块对应的文件。
modules
resolve.modules是一个数组,告诉Webpack去哪些目录下寻找第三方模块。之前提到过Webpack加载非核心模块的时候,需要写路径,但平时项目里引用的时候,并没有写路径,是因为这里做了统一处理。
module.exports = {
    resolve: {
        modules: [path.resolve(__dirname, 'node_modules')]
    }
}
alias
resolve.alias通过别名将原路径映射成一个新的路径。
module.exports = {
    resolve: {
        alias: {
            // 项目里绝对路径常用的@便是在这里配置
            '@': path.resolve(_dirname, 'src')
        }
    }
}
extensions
module.extensions是一个数组,用于拓展Webpack的文件支持项,之前说到过Webpack只能读取.js、.json、.node三种类型的文件,想要处理.ts、.vue、.jsx等类型的文件时,就需要做拓展。
module.exports = {
    resolve: {
        extensions: ['.ts', '.vue', '.jsx']
    }
}
devtool
devtool用于配置souce-map的精细程度,精细程度越高,打出来的包也越大。souce-map是打包后运行中的代码与本地代码之间的一个映射,改bug时,通过映射可以知道是本地哪块的代码出了问题。
module.exports = {
    // source-map/line-source-map/eval-source-map/hiden-source-map/cheap-source-map...
    // 下面是我们项目采用的配置,大家可以作为参考,放弃列,映射精确到行。
    devtool: 'cheap-module-eval-source-map'
}
devServer
只有安装了webpack-dev-server,通过devServer启服务时,配置才会生效。
proxy
proxy代理可以将浏览器到服务端的请求,转换成服务端与服务端之间的请求,绕过浏览器的同源性原则,从而避免跨域问题。
module.exports = {
    devServer: {
        proxy: 'http://localhost/demo:3000'
    }
}
总结
本来想写一篇实战总结的,结果快写完了,才发现似乎成了一篇知识归纳,干脆拆成两篇。
转载自:https://juejin.cn/post/7136895529354526756




