这篇文章给学校组织上课的一部分,同时也是为了帮我补充以前没注意的问题。
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 为出口打包
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-map | o | ++ | no | 原始源代码(仅限行) |
eval-source-map | -- | + | no | 原始源代码 |
cheap-source-map | + | o | yes | 转换过的代码(仅限行) |
cheap-module-source-map | o | - | yes | 原始源代码(仅限行) |
inline-cheap-source-map | + | o | no | 转换过的代码(仅限行) |
inline-cheap-module-source-map | o | - | 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 的打包工具
// ./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';