Webpack 是一个现代 JavaScript 应用程序的静态模块打包器 (module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图 (dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle
Webpack思想:一切皆模块
- ES2015 import 语句
- CommonJS require() 语句
- AMD define 和 require 语句
- css/sass/less 文件中的 @import 语句。
- 样式url(...))或 HTML 文件 中的图片链接
所有项目中使用到的依赖文件都被视为模块,webpack 做的就是把这些模块进行处理,进行一系列的 转换、压缩、合成、混淆 操作,把项目文件打包成最原始的静态资源。
简单体验
创建目录结构
mkdir webpack-demo
cd webpack-demo
npm init -y
mkdir dist
mkdir src
在 dist 目录下创建 index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<script src="main.js"></script>
<body>
</body>
</html>
在 src 目录下创建index.js
function timeout(time) {
return new Promise(resolve => {
setTimeout(resolve, time)
})
}
async function asyncPrint(value, time) {
await timeout(time)
console.log(value)
}
asyncPrint('hello,world', 5000)
安装webpack
在这之前你要安装好 nodejs,并保证她是 最新的版本 然后,进行以下步骤以保证你能位于中国的 pc 机上顺利的安装 npm 包。
- 设定中国镜像
npm install -g mirror-config-china --registry=http://registry.npm.taobao.org
# 检查是否安装成功
npm config list
- 安装 Windows build 环境(npm 上的有些包是 native 的,需要编译)
npm install -g windows-build-tools
//全局安装
npm install webpack webpack-cli -g
// **推荐** 局部安装,写入开发环境依赖
npm install webpack webpack-cli -D
执行webpack
# 以下代码生效基于webpack4零配置,只用于演示
# 如果为局部安装
npx webpack
# 使用 npx 命令,相当于他会在当前目录的 node_modules 的 .bin 目录下执行命令
# 此处未加 mode 选项,应该会有 warning,默认 mode 为production
# 增加mode后
npx webpack --mode development
webpack 会默认 src/index.js 文件为入口,dist/main.js 为出口打包
❯ npx webpack
Hash: ce7e1fea469219fad208 // 本次打包对应唯一一个hash值
Version: webpack 4.42.1 // 本次打包对应webpack版本
Time: 69ms // 消耗的时间
Built at: 2020-03-27 11:25:08 // 构建的时刻
Asset Size Chunks Chunk Names // 打包后的文件名,大小,分包的 id,入口文件名
main.js 1.02 KiB 0 [emitted] main
Entrypoint main = main.js
[0] ./src/index.js 245 bytes {0} [built]
正式开始
webpack4 拥有一些默认配置但是他并不是以零配置作为宣传口号,如果喜欢零配置使用的话,可以去看 parceljs,或者 ncc pkg(nodejs),而且对于不同的项目,我们往往需要高度的可定制性,这时候就需要我们自己写配置文件。
在项目目录下创建 webpack.config.js (默认配置文件地址), 可以通过 --config <文件> 显式指定
//常用配置模块
module.exports = {
entry: '', // 入口文件
output: {}, // 出口文件
devtool: '', // 错误映射 (source-map)
mode: '', // 模式配置 (production / development )
module: {}, // 处理对应模块 (对文件做些处理)
plugins: [], // 插件 (压缩/做其他事情的)
devServer: {}, // 开发服务器配置
optimization: {}, // 压缩和模块分离
resolve: {}, // 模块如何解析,路径别名
}
入口 (entry) 与出口 (output)
- 单入口单出口
以下就是 webpack 的默认配置
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.js'
}
};
写好配置文件后再次
npx webpack
效果不变
借用 npm script,在 package.json 中 scripts 属性增加 script 如下
{
...
"scripts": {
"dev": "webpack --mode development -w",
"build": "webpack --mode production"
}
...
}
- npm run dev -> webpack --mode development -w
- npm run build -> webpack --mode production
- 多入口与多出口
- 多入口多出口:多页面应用(MPA),打包多个js文件,不同页面分别引入。
- 单入口多出口:单页面应用(SPA),借助内置 splitChunksPlugins 模块进行代码分割,方便分离公共模块,
模式(mode)
module.exports = {
...
mode: 'development' || 'production'
...
}
mode 写入配置文件后,执行 webpack 时就不用再带 mode 选项
- development开发模式,即写代码的时候,在此模式下,为了提高开发效率,我们需要 提高编译速度,配置热更新和跨域,以及快速debug。
- production生产模式,即项目上线后,在此模式下,我们要 打包一份可部署代码,需要对代码进行压缩,拆分公共代码以及第三方js库
理解这两种模式容易,关键是根据不同的模式对 webpack 做不同的配置,因为不同模式下我们对代码的需求不一样。
开发项目时,通常会写两套不同的配置,一套用于开发环境,一套用于生产环境,两套不同配置包括三个配置文件,分别为
- 基础配置文件 webpack.config.js(包含开发与生产环境下都需要的配置)
- 开发环境配置文件 webpack.dev.js
- 生产环境配置文件 webpack.prod.js
以基础配置文件为入口,根据环境变量判断当前环境,使用 webpack-merge 插件融合相应环境配置文件。
npm install -D webpack-merge
//webpack.config.js
const path = require('path')
const merge = require('webpack-merge')
const devConfig = require('./webpack.dev.js')
const prodConfig = require('./webpack.prod.js')
const commonConfig = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
},
}
module.exports = env => {
if (env && env.production) {
return merge(commonConfig, prodConfig)
} else {
return merge(commonConfig, devConfig)
}
}
// webpack.dev.js
module.exports = {
mode: 'development',
output: {
filename: '[name].js',
},
};
// webpack.prod.js
module.exports = {
mode: 'production',
output: {
filename: '[name].[contenthash].js',
},
}
对scripts字段改写如下
因为我需要经常修改配置文件, 我们需要监控文件修改,然后重启 webpack,所以需要先安装 nodemon
npm install -D nodemon
{
"scripts": {
"dev": "nodemon --watch webpack.*.js --exec \"webpack -w\"",
"build": "webpack --env.production"
}
}
devtool(错误映射)(source-map)
devtool构建速度重新构建速度生产环境品质(quality)(none) yes打包后的代码eval no生成后的代码cheap-eval-source-map no转换过的代码(仅限行)cheap-module-eval-source-mapo no原始源代码(仅限行)eval-source-map-- no原始源代码cheap-source-map oyes转换过的代码(仅限行)cheap-module-source-mapo-yes原始源代码(仅限行)inline-cheap-source-map ono转换过的代码(仅限行)inline-cheap-module-source-mapo-no原始源代码(仅限行)source-map----yes原始源代码inline-source-map----no原始源代码hidden-source-map----yes原始源代码nosources-source-map----yes无源代码内容
在 webpack.dev.js 和 webpack.prod.js 中分别加入 source-map
//webpack.dev.js
module.exports = {
mode: 'development',
devtool: 'source-map',
output: {
filename: '[name].js',
}
}
//webpack.prod.js
module.exports = {
mode: 'production',
devtool: 'cheap-module-source-map',
output: {
filename: '[name].[contenthash].js'
}
}
plugins(插件)
插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。想要使用一个插件,你只需要 require() 它,然后把它添加到 plugins 数组中。多数插件可以通过选项 (option) 自定义, 有些插件你也可以在 一个单独配置文件 中进行配置。
需要通过使用 new 操作符来创建它的一个实例。
在我看来,plugins的主要作用有:
- 让打包过程更便捷 (提供一些帮助,如自动生成入口的 html 文件)
- 开发环境对打包进行优化,加快打包速度
- 生产环境压缩代码
两个简单插件示例
- html-webpack-plugin这个插件可以在打包完成后自动生成index.html文件,并将打包生成的 js、css 文件引入。
npm install html-webpack-plugin -D
- 新建 public 文件夹并创建 index.html 作为模板文件在 webpack.config.js 中
...
const HtmlWebpackPlugin = require('html-webpack-plugin');
...
const commonConfig = {
...
plugins: [
new HtmlWepackPlugin({
template: 'public/index.html',
}),
]
...
}
- clean-webpack-plugin自动清除上次打包生成的 dist 文件
npm install clean-webpack-plugin -D
- 在 webpack.prod.js 中
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
mode: 'production',
...
plugins: [new CleanWebpackPlugin()],
}
module (loader)(文件预处理)
loader 用于对模块的源代码进行转换。loader 可以使你在 import 或"加载"模块时预处理文件。因此,loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的强大方法。loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript 或将内联图像转换为 data URL。loader 甚至允许你直接在 JavaScript 模块中 import CSS文件!(官方)
在我看来,loader 的主要作用有:
- 处理图片、字体等资源
- 处理 css、预编译 sass/less/stylus
- 把 ES6 代码转义为 ES5
下面会引入 css、图片依赖,为使目录结构清晰,分别打包进单独文件夹
加载CSS
npm install css-loader style-loader mini-css-extract-plugin -D
在配置文件中加入 loader
注意:
- use字段下如果有多个 loader,从后至前依次执行
- 开发环境下使用 css-loader 和 style-loader 会把 CSS 写进 JS,然后 JS 添加样式,写在内联 style 里
- 生产环境下借助 webpack4 的 mini-css-extract-plugin 把CSS文件单独分离,link 引入
//webpack.dev.js
module.exports = {
mode: 'development',
devtool: 'cheap-module-eval-source-map',
output: {
filename: 'js/[name].js',
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
}
// webpack.prod.js
...
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
mode: 'production',
...
output: {
filename: 'js/[name].[contenthash].js', // 这里修改成 js 文件夹下面
},
module: {
rules: [
{
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '../',
}, // 文件打包至dist/css目录下,需配置 publicPath,以防等会引入图片出错
},
'css-loader',
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[hash:8].css', // css 样式打包到 css 文件夹下面
}),
...
],
}
处理 less
npm install less-loader less -D
// webpack.prod.js
...
module.exports = {
...
module: {
rules: [
{
test: /\.less$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '../',
}, // 文件打包至dist/css目录下,需配置publicPath,以防等会引入图片出错
},
'css-loader',
'less-loader',
]
}
],
},
...
}
// webpack.dev.js
module.exports = {
...
module: {
rules: [
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader'],
}
],
},
}
打包图片
在CSS等文件引入图片
npm install file-loader url-loader -D
在 webpack.config.js 中
...
const commonConfig = {
...
module: {
rules: [
{
test: /\.(jpe?g|png|gif|svg)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 8192,
outputPath: 'images/',
},
},
],
},
],
},
....
}
...
url-loader 配合 file-loader,在 options 中限制添加 limit 可以把指定大小图片编码成 base64,减少网络请求。
babel —— 转义ES6 代码
babel 默认只转换语法, 而不转换新的API, 如需使用新的API, 还需要使用对应的转换插件,例如,默认情况下babel可以将箭头函数,class 等语法转换为 ES5 兼容的形式,但是却不能转换 Map,Set,Promise等新的全局对象,这时候就需要使用 polyfill 去模拟这些新特性。
# babel 核心
npm install babel-loader @babel/core -D
# @babel/plugin-transform-runtime 这个会创建一些辅助函数,以防污染全局
# @babel/plugin-transform-regenerator async 转换
# @babel/runtime-corejs3 corejs 是一个 polyfill
npm install @babel/plugin-transform-runtime @babel/runtime-corejs3 @babel/plugin-transform-regenerator -D
在 webpack.config.js 中
...
const commonConfig = {
...
module: {
rules: [
{
test: /\.js$/,
exclude: '/node_modules/',
use: 'babel-loader',
},
...
},
...
}
...
在项目目录下新建 .babelrc.json 文件,写入options
这里只是为了演示方便而写的配置,并不符合实际,一般网页项目都和结合 @babel/preset-env 和 .browserslistrc 来使用,如果想看他们的区别请看 https://segmentfault.com/a/1190000021188054 写的比较清楚。
// .babelrc.json
{
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 3,
"proposals": true,
"useESModules": true
}
],
["@babel/plugin-transform-regenerator"]
]
}
devServer
npm install webpack-dev-server -D
每次编写完代码后,都要重新 npm run dev,为了提高开发效率,我们借助 webpack-dev-server 配置本地开发服务,主要字段如下:
// webpack.config.js
{
...
devServer: {
contentBase: './dist', //配置开发服务运行时的文件根目录
port: 3000, //端口
hot: true, //是否启用热更新
open: false, //是否自动打开浏览器
},
...
}
借助devServer,我们可以
- 方便的跑起本地服务而不用自己再去做资源处理
- 开发时借助代理实现跨域请求
- 开箱即用的 HMR(模块热更新)(需要 loader 支持,或者自己编写)
"scripts": {
"dev": "nodemon --watch webpack.*.js --exec \"webpack-dev-server\"",
"build": "webpack --env.production"
},
// webpack.dev.js
const webpack = require('webpack');
...
plugins: [
new webpack.HotModuleReplacementPlugin()
]
...
// ./src/test.js
export default () => {
console.log(1)
}
// ./src/index.js
import test from './test';
...
if (module.hot) {
module.hot.accept('./test.js', function() {
test();
})
}
test();
...
到这里就大概给大家介绍了一下 webpack 应该覆盖大部分内容,当然不可能这么快就熟悉,我觉得比较高效的学习方法就是 https://webpack.docschina.org/guides/installation
看 webpack 官方的 guides (写的非常仔细)走一遍应该就能比较好的理解了。
原理
我觉得实现一个简单的 bundler 是了解原理比较好的方法吧,所以这里就带大家来看怎么自己实现一个类似于 webpack 的打包工具,不要听着觉得很复杂,其实比较简单,因为这次没有做比较基础的一些工作了,直接调了别人的库。
// ./bundler.js
// nodejs 文件处理
const fs = require('fs');
// nodejs 文件路径
const path = require('path');
// 生成 ast 的库
const parser = require('@babel/parser');
// 遍历 ast
const traver = require('@babel/traverse').default;
// babel es6 -> es5
const babel = require('@babel/core');
/**
* 生成转义后的代码以及依赖关系
* @param filePath
* @returns {{code: string, filePath: string, dependencies: {}}}
*/
const moduleAnalyser = filePath => {
// 拿到文件内容
const content = fs.readFileSync(filePath, 'utf-8');
// 生成 ast
const ast = parser.parse(content, {
// 使用 es module
sourceType: 'module',
});
// 建立一个对象来接遍历的依赖
// key: relativePath -> 也就是 import xx from '<relativePath>'
// value: 唯一的绝对路径
const dependencies = {};
// 遍历 ast
traver(ast, {
ImportDeclaration ({ node }) {
const relativePath = node.source.value;
dependencies[relativePath] = path.join(path.dirname(filePath) relativePath.slice(1));
}
});
// 转义之后的代码
const { code } = babel.transformSync(content, {
presets: ['@babel/preset-env']
});
return {
filePath,
dependencies,
code,
};
};
/**
* 生成依赖关系图
* @param entry
* @returns {{
* dependencies: {}
* code: string
* }}
*/
const makeDependenciesGraph = entry => {
// 入口模块
const entryModule = moduleAnalyser(entry);
// 关系图
const graphArray = [entryModule];
for (let i = 0; i < graphArray.length; i ) {
// 拿到本次的 dependencies
const { dependencies } = graphArray[i];
if (dependencies) {
// 遍历 dependencies,推到关系图中
for (const j in dependencies) {
graphArray.push(moduleAnalyser(dependencies[j]));
}
}
}
const graph = {};
// 转换结构成对象
// key: 绝对路径
// value: dependencies, code
graphArray.forEach(item => {
graph[item.filePath] = {
dependencies: item.dependencies,
code: item.code
};
});
return graph;
};
/**
* 生成代码
* @param entry
* @returns {string}
*/
const generatorCode = entry => {
const graph = makeDependenciesGraph(entry);
console.log(graph);
return `
(function(graph){
function require(module) {
function localRequire(relativePath){
return require(graph[module].dependencies[relativePath])
}
var exports = {};
(function(require, code, exports){
eval(code)
})(localRequire, graph[module].code, exports);
return exports;
};
require(${JSON.stringify(entry)});
})(${JSON.stringify(graph)});
`
};
console.log(generatorCode(path.resolve(__dirname, 'src', 'index.js')));
// src/index.js
import message from './message.js';
console.log(message);
// src/message.js
import { word } from './word.js';
const message = `hello ${word}`;
export default message;
// src/word.js
export const word = 'world';
参考 https://juejin.im/post/5cb0948ce51d456e6154b3f4
最后再说一下 babel 到这里也没有结束,比如说 Babel handbook Babel Macros 之类的学习资源,而且他们都能做出能够帮助到你帮助到其他人的东西,而且为了解了原理之后为 webpack 编写 loader 和 plugins 都比较简单的。
,