在javascript刚刚流行时,前端项目通常比较简单,不需要考虑项目的开发效率、性能和扩展性等。
随着前端项目越来越复杂,需要更正式的软件开发实践,比如单元测试(unit testing)、代码检查(linting)、文件缩小(minification)、文件捆绑(bundling)和代码编译(compilation)等[1]。
- 单元测试确保代码修改不影响已有功能
- 代码检查保证一致的代码风格,没有错误
- 文件压缩用于提高资源的访问速度,比如jpg/png、js和css等
- 文件捆绑可以解决页面异步请求数百个js和css文件而导致的性能下降。因为每个异步请求都会有微小开销(请求头、握手等)[1],通常将这些js和css分别捆绑到一个文件中,这样将会请求一个单独的JS和CSS文件而不是数百个单独的文件
- 代码编译是指语言预处理和转译等,比如语言预处理器SASS和JSX将CSS和JS编译成原生的,转译器Babel将ES6转译为ES5获取更好的兼容性
将上面的开发实践看成一个个任务,这些任务与web应用的逻辑没有关联,开发这些任务也会耗费大量时间和精力。所以像grunt这样的构建工具出现了,可以通过一个命令依次运行多个任务。这些任务自动化地在开发环境运行,开发者可以专注于写应用的代码。[1]grunt的出现是跨时代的。在它之前我们经常都是通过 bash 或者 make 调用 closure-compiler 之类的工具。前端并不存在一个统一的构建工具和标准,甚至我们自己写过一些简单的构建工具。[2]
gulp的功能类似于grunt,可以通过一个命令运行多个任务。但gulp是基于Node stream的,一个任务中的多个操作在内存中进行的;相比于grunt执行一个任务中的多个操作时,每个操作后将临时文件输出到硬盘更高效。[3]gulp优化了任务的配置,gulp的配置更简单,代码量也更少。
当开始使用node中require()或import写浏览器代码[1],并加载npm安装的模块时,需要打包工具webpack或browserify。node样式的代码无法直接在浏览器运行。webpack或browerify会递归解析所有的require()或import的js文件,然后捆绑到一个可以通过引入的js文件中,浏览器可以正常运行该文件。[4]相比browserify,webpack还实现了gulp/grunt中的大量的任务功能[1],webpack可以单独使用。而browerify的功能单一,通常和gulp/grunt搭配使用。
webpack是一个强大的工具,除了模块捆绑,还实现了gulp/grunt中的大量的任务功能。Webpack实现了捆绑后的文件的代码缩小和sourceMap。webpack可以作为一个中间件运行在webpack-dev-server上,它支持热更新和实时更新。[1]一些系统或框架内置了webpack工具,比如通过vue-cli创建的脚手架、angular框架[3]等。所以了解webpack的原理是十分重要的。本文以简单的vue-cli项目为例,从webpack的打包入口开始,详细介绍webpack的打包流程,源文件时如何被打包的,打包后的文件格式是怎样的。
在介绍webpack打包vue-cli项目之前,先介绍webpack打包html和js的简单项目。
1. 环境安装#
vue-cli是一个用于快速Vue.js开发的完整系统。在介绍vue-cli项目的打包之前,需要先进行环境安装。安装时需要注意Node.js和包之间以及包和包之间的版本兼容性。通常github仓库下就有相应的兼容性信息。本文安装的Node.js和各包的版本如图1。
| 软件或包的名称 | Node.js | npm(包含在Node.js中) | @vue-cli | webpack | webpack-dev-server |
|---|---|---|---|---|---|
| 版本 | v16.20.2 | 8.19.4 | 5.0.8 | 3.12.0 | 2.11.5 |
图1 Node.js版本及npm安装的包版本
1.1 Node.js#
Node.js是基于V8引擎的JavaScript开发环境[5]。当和其它的服务端语言(PHP和Python等)结合起来,就可以构建成熟的web应用程序。Node.js中的原生模块(比如http、path和Stream模块等)通常是使用C++实现的,在安装时使用Python构建工具(gyp)编译成类似DLL的内容,Node.js在运行时可为目标环境加载该模块。[6]Demo1是Node.js最常见的案例HelloWorld[7],它实现了一个web服务器,其中使用require加载原生http模块,调用http模块的createServer创建服务器,服务器监听域名为127.0.0.1端口号为3000的http请求。Node.js中的require函数不能直接在浏览器运行,直接在浏览器运行会报错,如图2。Demo1修改为ES6模块导入语法后,因为浏览器不存在http模块,在浏览器运行也会报错,如图3。
//CommonJS语法加载模块
const http = require('node:http');
//ES6语法加载模块
//import http from 'http';
const hostname = '127.0.0.1';
const port = 3000;
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello World\n');
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
Demo1 Node.js最常见的案例HelloWorld
图2 Node.js中的require函数在浏览器运行报错
图3 浏览器不存在http模块,运行报错
1.2 npm#
npm是世界上最大的软件注册中心。世界各地的开源开发者使用npm分享和借鉴包,一些组织也使用npm管理私有开发。npm包含3个部分,npm网站、npm命令行工具和npm注册中心。npm网站用于发现包,设置配置文件和管理npm使用的其它方面,比如你可以设置组织来管理对公有和私有包的访问。npm命令行工具用于在终端运行命令,是大多数开发者和npm交互的途径。npm注册中心是一个包含JavaScript软件及其周围元信息的大型公共数据库。[8]
要使用npm命令行工具,需要先安装npm。你需要安装Node.js和npm命令行工具,目前Node.js软件包中已经包含了npm工具。建议你使用 nvm安装Node.js[9],以方便进行Node.js的版本切换。安装完成后,可以使用node -v和npm -v命令检查是否安装成功。
可以使用npm install来下载和安装包。常见的用途有以下3个。
- 使用npm install下载包,在自己的模块中通过require或import依赖该包;
- 使用npm install下载和安装包,安装包后在node_modules/.bin目录下生成相应的.cmd文件,可以通过npx命令(详见5.2节)运行包;比如webpack,webpack-dev-server包的安装;
- 使用npm install -g全局下载和安装包,安装后在node的安装目录下生成.cmd文件,在终端可以像运行node命令一样运行包。比如使用
npm install vue-cli -g全局安装vue-cli后,在node的安装目录下生成vue.cmd文件,如果node安装目录在环境变量Path中配置了,可以直接在终端运行vue -v检查vue-cli是否安装成功。
1.3 @vue-cli#
CLI(@vue/cli)是全局安装的npm包,安装后可在终端使用vue命令,通过vue create命令可快速创建项目的脚手架[10]。如Demo2,使用npm install -g @vue/cli安装vue-cli,安装后使用vue -v检查是否安装成功。vue init是vue2中的命令,如果在vue2中,可直接使用vue init webpack my-project快速创建项目的脚手架;如果要在vue>=3中使用vue init,需要先通过npm install -g @vue/cli-init安装全局桥[11],然后使用vue init webpack my-project创建项目的脚手架,创建时需要进行如图4的一些配置。实际在使用vue init webpack my-project命令时可能会网络超时,需要先修复网络超时的问题或采用离线方式初始化[12]。
创建的项目脚手架my-project中,使用webpack作为打包工具,webpack的配置文件中已经做好了常用配置,你不用花很多时间在配置webpack上,可以更专心地进行应用的开发。
npm install -g @vue/cli
vue -v
#安装全局桥
npm install -g @vue/cli-init
vue init webpack my-project
Demo2 vue-cli安装及基于vue-cli创建项目脚手架
图4 基于vue-clli创建项目脚手架时的配置
1.4 webpack,webpack-cli#
vue-cli项目中已经添加了webpack包的依赖,使用npm install即可安装好项目依赖的所有包。项目中已经添加了webpack的配置文件,并做好了常用配置,可以直接使用npm run dev运行项目,或者使用npm run build打包项目。如Demo3,npm run build实际执行的是node build/build.js命令[13],build.js中使用调用webpack()函数进行项目打包。
"scripts": {
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"start": "npm run dev",
"build": "node build/build.js"
},
Demo3 项目package.json中的配置
如果你想单独安装webpack,并进行webpack的简单使用,可参考第2节。在单独安装webpack的时候,官方会建议同时安装webpack-cli。webpack运行时将配置文件中的配置解析为options,webpack-cli提供了修改options的接口。webpack-cli的options会覆盖配置文件中的options。webpack-cli提供了丰富的命令帮助你更快地开发应用。比如webpack-cli b表示运行webpack,webpack-cli t表示对webpack配置文件进行验证,webpack-cli c E:\myproject表示在目录E:\myproject下创建一个新的webpack项目。[14]
1.5 webpack-dev-server#
webpack-dev-server为你提供了基本的web服务器和实时重新加载的能力[15]。可以使用webpack-dev-server方便地进行开发调试。
vue-cli项目在执行如1.4节的npm install会安装webpack-dev-server,然后可以直接使用npm run dev运行项目。如1.4节的Demo3,npm run dev实际执行的是webpack-dev-server --inline --progress --config build/webpack.dev.conf.js命令(详见5.2节)。
2. webpack的简单使用#
本节通过一个简单案例,学习webpack的简单使用。这个案例从基础的HTML和JS,到使用ES6进行改造,到在项目中使用webpack。使用webpack后,项目的文件目录发生变化,最终浏览器访问的文件格式也发生变化。从输出的文件可以看出webpack的一些特点,比如代码缩小和模块捆绑等。本节使用的webpack版本号为5.90.0。
2.1 使用HTML和JS的简单项目[16]#
如Demo4先创建一个空项目,并进行项目初始化,然后安装webpack和webpack-cli。安装后项目目录如图5。
mkdir webpack-demo
cd webpack-demo
npm init -y
npm install webpack webpack-cli --save-dev
Demo4 简单案例的环境准备
图5 环境准备好后的项目目录
在项目中新增index.html和index.js文件,如图6。如Demo5,index.html中同时引入了lodash.js和index.js两个js文件;其中index.js使用了lodash.js中的字符串拼接函数_.join,如Demo6。可以直接在浏览器访问index.html。
图6 在项目中新增index.html和index.js文件
html>
<html>
<head>
<meta charset="utf-8" />
<title>Getting Startedtitle>
<script src="https://unpkg.com/lodash@4.17.20/lodash.js">script>
head>
<body>
<script src="./src/index.js">script>
body>
html>
Demo5 项目文件index.html
function component() {
const element = document.createElement('div');
// Lodash, currently included via a script, is required for this line to work
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());
Demo6 项目文件.src/index.js
2.2 基于ES6修改简单项目#
ES6 在语言标准的层面上,实现了模块功能。使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块[17]。基于ES6的模块功能修改简单案例。改写的时候要注意,因为lodash.js是Node.js中一个CommonJS模块,但浏览器不支持CommonJS模块,lodash.js在浏览器不能正常编译运行(详见5.5节)。具体的修改步骤如下:
- 下载远程库文件loadsh.js文件[18];将loadsh.js由CommonJs格式改写为ES6格式(如Demo7),并放在./lib目录下;
- 改写index.html和index.js,改写后如Demo8和Demo9所示。如Demo8,浏览器加载 ES6 模块,
标签要加入type="module"属性[19]。
var arrayProto = Array.prototype;
var nativeJoin = arrayProto.join;
export function join(array, separator) {
return array == null ? '' : nativeJoin.call(array, separator);
}
Demo7 由CommonJS模块lodash.js改写成的ES6模块
html>
<html>
<head>
<meta charset="utf-8" />
<title>Getting Startedtitle>
head>
<body>
<script type="module" src="./src/index.js">script>
body>
html>
Demo8 项目文件index.html
/*使用ES6的import输入函数join*/
import {join} from './lib/lodash.js'
function component() {
const element = document.createElement('div');
element.innerHTML = join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());
Demo9 项目文件.src/index.js
修改后,不可直接浏览器直接访问index.html,因为ES6遵循同源策略,如果直接访问会报错”Access to Script at ' from origin 'null' has been blocked by CORS policy“[66]。可以将静态资源放到nginx服务器上进行访问。
相比于基础的HTML和JS,使用ES6的import输入函数有2个优点[16]:
- 可以明确的看到index.js中依赖哪些JS文件;
- JS文件的加载顺序也很明确;在2.1节的案例中,JS文件的相互依赖不清晰,可能因为JS文件的加载顺序不对,而导致程序错误。
2.3 在简单项目中使用webpack#
除了浏览器直接加载ES6模块,也可以在项目中使用webpack,在源代码中使用node样式(ES6或CommonJS),生成的发布物代码中不包含node样式。在简单项目中使用webpack后,项目将会分为源代码(./src)和发布物代码(./dist)两个文件夹,如图7。实际开发时在./src下创建和编辑文件;项目构建后的经过压缩和优化的代码会输出到./dist下,最终在浏览器加载的是./dist下文件。[16]
在项目中使用webpack,需要进行以下操作:
- 使用
npm install --save lodash安装lodash包。 - 创建文件夹./dist。将index.html移到./dist下。并对2.1节的index.html和index.js分别做如Demo10和Demo11的修改。
- 使用npx webpack 进行打包。打包后的项目目录如图8。npx运行webpack的原理其实很简单,详见5.2节。
图7 项目目录
html>
<html>
<head>
<meta charset="utf-8" />
<title>Getting Startedtitle>
head>
<body>
<script src="main.js">
script>
body>
html>
Demo10 项目文件dist/index.html
import _ from 'lodash';
function component() {
const element = document.createElement('div');
// Lodash, now imported by this script
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());
Demo11 项目文件src/index.js
图8 webpack打包后的文件main.js
如图8,webpack将index.js及其依赖的模块lodash都打包进了main.js文件。并进行了代码缩小。总结起来,使用webpack后主要有2个优点:
- 将node样式的代码(import或require)转译为浏览器可运行的代码。从配置的入口文件开始,将文件所有的依赖模块捆绑到一个js文件中。生成的捆绑文件是完全独立的,包含应用所需的所有信息,而且开销很小。
- 代码缩小。代码缩小用于提高资源的访问速度。
你可能发现了index.html是手动在dist文件下创建的,这里只是做简单演示。当webpack配置HtmlWebpackPlugin插件后,dist下的index.html会在打包时自动生成。在第3节打包vue-cli项目时,就不用对dist下的文件做任何修改。
除了上述了2个优点外,还可以通过配置loader和plugin等来增强webpack的功能。比如配置vue-loader后,webpack可以加载扩展名为.vue的文件;配置html-webpack-plugin插件后,目标目录下的index.html会在打包时自动生成。
2.4 使用webpack的配置文件#
webpack4.0在使用时可以不进行任何配置,使用默认配置,但实际项目中通常需要更复杂的配置。可以使用配置文件对webpack进行配置,相比于在终端命令行中配置要简单高效。如图9,在项目中添加了webpack.conf.js文件,文件中配置了打包的入口和出口,如Demo12。执行npx webpack命令,可以正常打包。
还可以在配置文件中配置loader rules、plugin、resolve options等,实际项目的配置通常比较复杂。
图9 在项目中添加的webpack.conf.js文件
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
};
Demo12 项目文件webpack.conf.js
实际运行webpack时可能有不同的命令,本节的项目中使用的是npx webpack,第3节中的vue-cli项目使用的是node build/build.js。可以在package.json的scripts对象下进行配置,通过统一的命令运行webpack。如图10,在scripts中配置build属性后,可以通过npm run build来运行webpack[13]。注意scripts中build属性的值是”webpack“,而不是”npx webpack“,原因详见5.2节。
图10 在package.json配置scripts对象
3. webpack打包vue-cli项目#
本节基于在1.3节通过vue-cli创建的项目脚手架(简称为vue-cli项目),学习webpack的打包流程。vue-cli项目的项目目录如图11所示。
图11 vue-cli项目的目录
3.1 源码流程图#
为了简化源码流程图,在实际调试的时候先将webpack.prod.conf.js中的UglifyJsPlugin、ExtractTextPlugin、OptimizeCSSPlugin和HtmlWebpackPlugin注释掉,如Demo13。
plugins: [
// http://vuejs.github.io/vue-loader/en/workflow/production.html
new webpack.DefinePlugin({
'process.env': env
}),
/*new UglifyJsPlugin({
uglifyOptions: {
compress: {
warnings: false
}
},
sourceMap: config.build.productionSourceMap,
parallel: true
}),*/
// extract css into its own file
/* new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css'),
// Setting the following option to `false` will not extract CSS from codesplit chunks.
// Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
// It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,
// increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
allChunks: false,
}),
// Compress extracted CSS. We are using this plugin so that possible
// duplicated CSS from different components can be deduped.
new OptimizeCSSPlugin({
cssProcessorOptions: config.build.productionSourceMap
? { safe: true, map: { inline: false } }
: { safe: true }
}),*/
// generate dist index.html with correct asset hash for caching.
// you can customize output by editing /index.html
// see https://github.com/ampedandwired/html-webpack-plugin
/* new HtmlWebpackPlugin({
filename: config.build.index,
template: 'index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
// more options:
// https://github.com/kangax/html-minifier#options-quick-reference
},
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency'
}),*/
// keep module.id stable when vendor modules does not change
new webpack.HashedModuleIdsPlugin()
]
Demo13 将webpack.prod.conf.js中的UglifyJsPlugin、ExtractTextPlugin、OptimizeCSSPlugin和HtmlWebpackPlugin注释掉
webpack的打包流程可分2个阶段,一是从一个或多个入口文件开始构建依赖关系图,如流程图1;二是将项目所需的所有模块合并到一个或多个捆绑文件中,捆绑文件是包含所有内容的最终发布物,如流程图2。
如流程图1,终端运行npm run build命令后,实际执行的命令是node build/build.js。在build.js文件中,调用了webpack函数。webpack函数中调用new WebpackOptionsApply().process()注册了make函数,然后通过compiler.run()调用了make函数。在make函数中,主要进行的操作如流程图1中的步骤①-④。根据入口文件entry创建module;构建module;为module的依赖创建和构建module;递归为module的依赖创建和构建module。在经过步骤①-④后,从入口文件开始完整构建了依赖关系图,同时还为部分依赖创建和构建了module。入口文件的module创建是在步骤①中的moduleFactory.create()方法,module的构建是在步骤②的this.buildModule()方法。依赖文件的module创建是步骤③中的factory.create()方法,module的构建是步骤④中的_this.buildModule方法,具体的细节详见3.2.1.2节。在依赖关系图构建完成后,会调用callback函数,callback函数回调的流程从“callback函数回调1”开始,直到"callback函数回调3的函数体"结束。在"callback函数回调3的函数体"中,会调用compilation.seal方法将所有模块合并到一个或多个捆绑文件中,详见流程图2。
流程图1 从入口文件entry开始构建依赖关系图
如流程图2,compilation.seal方法主要包括步骤①-⑤。其中步骤①-③是模块捆绑前的准备,获取依赖中的所有module;将module分别放入app、mainifest和vendor等3个chunk中;将app的chunk中module分为NormalModule和ConcatenatedModule。步骤④将根据代码生成的module捆绑到app.js(app.[chunkhash].js的简写)的ConcatSource中;将函数“webpackJsonp”和__webpack_require__捆绑到manifest.js(manifest.[chunkhash].js的简写)的ConcatSource中;将通过require或import引入的所有node_moudes下的module捆绑到vendor.js(vendor.[chunkhash].js的简写)的ConcatSource中。步骤⑤分别基于app.js、manifest.js和vendor.js的ConcatSource生成一个RawSource,RawSource最终被写入目标目录下的相应js文件中。执行完compilation.seal后,开始调用回调函数。回调函数中调用this.emitAssets将发布物文件输出到目标目录下,输出细节详见步骤⑥。
流程图2 将项目所需的所有模块合并到3个捆绑文件中
虽然整个流程看着并不复杂,但实际调试的过程中遇到了很多问题。首先,webpack是基于plugin的,源码中通过plugin()注册了很多函数,并通过applyPlugins()(或者applyPluginsAsync和applyPluginsParallel等)调用这些函数。applyPlugins()通常和plugin()在不同的js文件中,调试源码时,需要根据applyPlugins()找到plugin()的位置。根据applyPlugins()找到plugin()的位置通常需要2步,第一步根据applyPlugins()参数中的函数名找到plugin()注册函数的代码位置,需要确认applyPlugins()和plugin()的调用对象是同一个;第二步在找到plugin()的位置后,plugin()的位置通常有多个,在plugin()注册的函数体中打断点,确认在调试的时候是否会进入plugin()中,因为必须在applyPlugins()之前已经调用了plugin()所在的plugin中的apply()方法进行了函数的注册,applyPlugins()才能正常调用plugin()参数中的函数;如果能正常进入到plugin()参数中的函数体内,则已经进行了函数的注册,可以再查找下调用plugin的apply()方法进行函数注册的位置。
webpack运行时调用了很多的applyPlugins,如果每个applyPlugins都通过这种方式进行查找和调试,是比较耗时的。如何快速地找到webpack中主要流程的代码呢?我是偶然的机会找到webpack的主要流程的。在刚开始尝试阅读webpack源码时,笔者从wepback的运行入口node build/build.js开始调试,发现在源码中有很多applyPlugins()的调用,同时也有plugin中apply方法的调用,比如流程图1中的new WebpackOptionsApply().process()就通过compiler.apply()调用很多plugin的apply方法进行函数注册,这些plugin有JsonpTemplatePlugin、FunctionModuleTemplatePlugin和NodeSourcePlugin等。当时就对这些plugin的用途很感兴趣。就选择对其中的JsonTemplatePlugin.js和FunctionModuleTemplatePlugin.js进行调试,查看其中注册的函数在调用时的入参。在调试FunctionModuleTemplatePlugin.js中的render函数时,发现入参中有项目代码的痕迹(如图12),所以判断render函数是主要流程中的一个环节。从FunctionModuleTemplatePlugin.js中的render函数开始进行逐步调试,render函数调用结束返回到ModuleTemplate的render函数中,ModuleTemplate的render函数调用结束后返回到Template的renderChunkModules()函数中,基于函数调用结束后会返回到上一层函数中,最后调试到了Compilation的seal方法和Compiler的compile方法中。然后根据上述调试流程梳理出整个打包流程的雏形。
图12 文件FunctionModuleTemplatePlugin.js中的render函数入参moduleSource
其次,Node.js中的异步操作使得代码调试变得复杂。Node.js在发生IO操作时是异步的。Node.js不会等这个IO操作结束才去执行接下来的操作,而是直接去执行后续的操作[20]。webpack打包时有读写文件的IO操作,在源代码调试时需要注意这些IO操作。比如流程图5中调用_this.buildModule()进行module构建,构建完成后会调用回调函数。_this.buildModule()执行时会进行IO操作,IO操作具体在LoaderRunner.js的函数processResource中。IO操作的先后顺序与IO操作的回调函数的顺序不一定是一致的(详见5.7节),这使得_this.buildModule()方法的执行顺序与其回调函数的顺序不一定一致。在基于vue-cli项目调试webpack的源代码时,根据入口文件构建依赖关系图的流程中会多次调用_this.buildModule()方法,如流程图1。图13对部分_this.buildModule()和其回调函数的执行次序进行了记录。显然,_this.buildModule方法的回调函数不一定是紧接着_this.buildModule方法执行的;它的执行顺序与_this.buildModule方法也不完全一致。当调试某个dependentModule的_this.buildModule方法和其回调函数时会变得更复杂,需要花费更多的时间调试到_this.buildModule方法和其回调函数的调用。
| 入参dependentModule的rawRequest | _this.buildModule的执行序号(与回调函数的执行一并排序) | 回调函数的执行序号(与_this.buildModule的执行一并排序) |
|---|---|---|
| vue | 1 | 2 |
| ./../../webpack/buildin/global.js | 3 | 5 |
| ./App | 4 | 6 |
| !!babel-loader!../node_modules/vue-loader/lib/selector?type=script&index=0!./App.vue | 7 | 8 |
| !../node_modules/vue-loader/lib/component-normalizer | 9 | 12 |
| !!../node_modules/vue-loader/lib/template-compiler/index?{"id":"data-v-5c20e860","hasScoped":false,"transformToRequire":{"video":["src","poster"],"source":"src","img":"src","image":"xlink:href"},"buble":{"transforms":{}}}!../node_modules/vue-loader/lib/selector?type=template&index=0!./App.vue | 10 | 11 |
| ./components/HelloWorld | 13 | 16 |
| !!../node_modules/extract-text-webpack-plugin/dist/loader.js?{"omit":1,"remove":true}!vue-style-loader!css-loader?{"sourceMap":true}!../node_modules/vue-loader/lib/style-compiler/index?{"vue":true,"id":"data-v-5c20e860","scoped":false,"hasInlineConfig":false}!../node_modules/vue-loader/lib/selector?type=styles&index=0!./App.vue | 14 | 15 |
图13 部分_this.buildModule()和其回调函数的执行次序
最后,webpack的部分源码很难读懂。笔者只是对webpack的打包流程有个大概了解,对很多webapck的细节还未读懂。比如进行模块捆绑的createChunkAssets函数,插件uglifyjs-webpack-plugin中的方法asset.sourceAndMap()和runner.runTasks()等。同时,根据entry文件构建依赖关系图的过程中一些方法的源码还未全面读过,比如module的创建方法和解析module中依赖的方法。
3.2 源码中的重要细节#
3.2.1 从entry到outputPath下的文件#
如流程图3,从entry文件到outputPath下的文件经过了几个步骤。从入口文件entry(包含在dependency中,详见流程图1)开始,每个步骤依次对输入的数据做不同的处理,最终生成了发布物this.assets,并输出到目标目录outputPath下。下面对整个流程中每个节点的数据进行了记录,并对步骤”流程图1①-④“的源码进行分析。对其它步骤的源码也尝试阅读过,但阅读的时候还有很多不懂的问题,这里先不深入源码。
流程图3 从entry文件到outputPath下的文件经历的步骤
3.2.1.1 dependency(包含entry)#
如流程图1①,根据入口文件dependency(包含entry)创建module,其中dependency的值如图14。
图14 流程图1①中_addModuleChain函数的入参dependency
3.2.1.2 this.preparedchunks#
入口文件entry经过流程图1①-④步骤处理后,生成的this.preparedChunks的值如图15。rawRequest为./src/main.js的module有6个dependency,索引为2的dependency的request为./App,该denpendency对应App.vue,该denpendency下有13个denpendencies。rawRequest为./App的module下的索引为5的dependency的request为!!babel-loader!../node_modules/vue-loader/lib/selector?type=script&index=0!./App.vue,表示对app.vue中部分的处理,该dependency下有5个dependencies,如图16。!!babel-loader!../node_modules/vue-loader/lib/selector?type=script&index=0!./App.vue下的索引为1的dependency的request为./components/HelloWorld,该dependency对应Helloworld.vue,该denpendency下有13个denpendencies,如图16。rawRequest为./components/HelloWorld的module下的索引为5的dependency的request为!!babel-loader!../../node_modules/vue-loader/lib/selector?type=script&index=0!./HelloWorld.vue,表示对Helloworld.vue中部分的处理,该dependency下有3个dependencies,如图17。将所有dependencies的个数相加,共有(6+13+5+13+3)等于40个dependency。
图15 入口文件entry经过流程图1中的步骤后生成的this.preparedChunks
图16 入口文件entry经过流程图1中的步骤后生成的this.preparedChunks
图17 入口文件entry经过流程图1中的步骤后生成的this.preparedChunks
如图15,request为"./App"的dependency构建生成了NormalModule,且module下有13个dependencies。rawRequest为"./App"的NormalModule及module下的dependency是如何生成的呢?request为"./App"的dependency是入口文件的依赖,由dependency生成module可分为流程图1中③和④两个步骤。如流程图1③,在addModuleDependencies函数中调用factory.create()方法创建module;如流程图1④,在factory.create()创建module成功后的回调函数中,调用this.buildModule方法构建module。如果module有dependency,则调用processModuleDependencies递归创建和构建module。request为"./App"的dependency是入口文件的依赖,它的NormalModule的创建也是从processModuleDependencies开始的,module创建流程详见流程图4,构建流程详见流程图5。
流程图4中,processModuleDependencies方法中调用了addModuleDependencies方法,addModuleDependecies方法中调用factory.create方法创建module。factory.create方法中包含函数"factory","resolve"的执行。函数"resolve"执行时,会根据源文件E:\hello-vue-cli\src\App.vue的文件扩展名获取相应的loader,相应的loader为E:\hello-vue-cli\node_modules\vue-loader\index.js??ref--0。module创建成功后,会调用_this.buildModule进入module构建流程。
流程图4 从processModuleDependencies开始的module创建流程
流程图5中,_this.buildModule方法中进行了module构建,构建时会先迭代所有的loader,并按索引由低到高依次执行所有的pitchLoader(步骤2-1),然后按索引由高到低并依次执行所有的normalLoader(步骤2-2),这些loader对源文件进行预处理,生成预处理代码。源文件E:\hello-vue-cli\src\App.vue的代码如Demo14,loader为E:\hello-vue-cli\node_modules\vue-loader\index.js??ref--0,loader预处理后的代码如Demo15。如流程图5步骤3,this.doBuild的回调函数中调用this.parser.parse()方法,将预处理代码赋予module对象的_source._value属性中,解析预处理代码中的dependency并赋予到module对象的dependencies属性中。经过步骤3处理后,rawRequest为”./App“的module的dependencies如图1。在_this.buildModule方法执行完后,调用callback函数,callback函数的调用从流程图5中的”callback函数回调1“开始,直到”callback回调函数6的函数体“结束。在”callback回调函数6的函数体“中,又调用了processModuleDependencies方法,processModuleDependencies是递归调用的,直到无法由dependency创建module或module下没有dependency时递归终止。
流程图5 从_this.buildModule开始的module构建流程
图15中rawRequest为”./App“的module下索引为8的dependency的request是!!../node_modules/vue-loader/lib/template-compiler/index?{"id":"data-v-6cbc4d12","hasScoped":false,"transformToRequire":{"video":["src","poster"],"source":"src","img":"src","image":"xlink:href"},"buble":{"transforms":{}}}!../node_modules/vue-loader/lib/selector?type=template&index=0!./App.vue,该dependency表示"./App.vue"文件的template部分。该dependency的module构建流程与request为”./App“的NormalModule的构建流程相似,如流程图4和5。该dependency的源代码也是E:\hello-vue-cli\src\App.vue,先后执行loader函数E:\hello-vue-cli\node_modules\vue-loader\lib\selector.js?type=template&index=0和E:\hello-vue-cli\node_modules\vue-loader\lib\template-compiler\index.js;然后经过buildModule后,生成的预处理代码如Demo16。Demo16中已经将template处理为render函数;在html中直接引入vue.js的案例[20]中,将template处理为render函数是在浏览器加载的时候才执行的。在打包的时候就将template处理为render函数,减少了浏览器的性能开销。
<div id="app">
<img src="./assets/logo.png">
<HelloWorld/>
<button v-on:click="handleClick">button>
div>
template>
<script>
import HelloWorld from './components/HelloWorld'
export default {
name: 'App',
components: {
HelloWorld
},
methods: {
handleClick() {
alert(1)
}
}
}
script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
style>
Demo14 源文件E:\hello-vue-cli\src\App.vue的代码
function injectStyle (ssrContext) {
require("!!../node_modules/extract-text-webpack-plugin/dist/loader.js?{\"omit\":1,\"remove\":true}!vue-style-loader!css-loader?{\"sourceMap\":true}!../node_modules/vue-loader/lib/style-compiler/index?{\"vue\":true,\"id\":\"data-v-6cbc4d12\",\"scoped\":false,\"hasInlineConfig\":false}!../node_modules/vue-loader/lib/selector?type=styles&index=0!./App.vue")
}
var normalizeComponent = require("!../node_modules/vue-loader/lib/component-normalizer")
/* script */
export * from "!!babel-loader!../node_modules/vue-loader/lib/selector?type=script&index=0!./App.vue"
import __vue_script__ from "!!babel-loader!../node_modules/vue-loader/lib/selector?type=script&index=0!./App.vue"
/* template */
import __vue_template__ from "!!../node_modules/vue-loader/lib/template-compiler/index?{\"id\":\"data-v-6cbc4d12\",\"hasScoped\":false,\"transformToRequire\":{\"video\":[\"src\",\"poster\"],\"source\":\"src\",\"img\":\"src\",\"image\":\"xlink:href\"},\"buble\":{\"transforms\":{}}}!../node_modules/vue-loader/lib/selector?type=template&index=0!./App.vue"
/* template functional */
var __vue_template_functional__ = false
/* styles */
var __vue_styles__ = injectStyle
/* scopeId */
var __vue_scopeId__ = null
/* moduleIdentifier (server only) */
var __vue_module_identifier__ = null
var Component = normalizeComponent(
__vue_script__,
__vue_template__,
__vue_template_functional__,
__vue_styles__,
__vue_scopeId__,
__vue_module_identifier__
)
export default Component.exports
Demo15 源文件App.vue经过vue-loader/index.js处理后的代码
var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{attrs:{"id":"app"}},[_c('img',{attrs:{"src":require("./assets/logo.png")}}),_vm._v(" "),_c('HelloWorld'),_vm._v(" "),_c('button',{on:{"click":_vm.handleClick}})],1)}
var staticRenderFns = []
var esExports = { render: render, staticRenderFns: staticRenderFns }
export default esExports
Demo16 源文件App.vue经过vue-loader\lib\selector.js?type=template&index=0和vue-loader\lib\template-compiler\index.js处理后的代码
3.2.1.3 this.chunks(1)#
如流程图2①,在函数this.processDependenciesBlocksForChunks执行完后,将this.preparedChunks中所有dependecies下的module抽取到this.chunks中,共13个module,如图18。这13个module的request分别如图19所示。这里有个疑问,this.chunks中的索引为12的module的含义是什么,暂未在dependecies下找到该module。
图18 流程图2①步骤执行后的this.chunks
| n | _modules[n].request |
|---|---|
| 0 | E:\hello-vue-cli\node_modules\babel-loader\lib\index.js!E:\hello-vue-cli\src\main.js |
| 1 | E:\hello-vue-cli\node_modules\vue\dist\vue.esm.js |
| 2 | E:\hello-vue-cli\node_modules\vue-loader\index.js??ref--0!E:\hello-vue-cli\src\App.vue |
| 3 | E:\hello-vue-cli\node_modules\extract-text-webpack-plugin\dist\loader.js?{"omit":1,"remove":true}!E:\hello-vue-cli\node_modules\vue-style-loader\index.js!E:\hello-vue-cli\node_modules\css-loader\index.js?{"sourceMap":true}!E:\hello-vue-cli\node_modules\vue-loader\lib\style-compiler\index.js?{"vue":true,"id":"data-v-6cbc4d12","scoped":false,"hasInlineConfig":false}!E:\hello-vue-cli\node_modules\vue-loader\lib\selector.js?type=styles&index=0!E:\hello-vue-cli\src\App.vue |
| 4 | E:\hello-vue-cli\node_modules\vue-loader\lib\component-normalizer.js |
| 5 | E:\hello-vue-cli\node_modules\babel-loader\lib\index.js!E:\hello-vue-cli\node_modules\vue-loader\lib\selector.js?type=script&index=0!E:\hello-vue-cli\src\App.vue |
| 6 | E:\hello-vue-cli\node_modules\vue-loader\lib\template-compiler\index.js?{"id":"data-v-6cbc4d12","hasScoped":false,"transformToRequire":{"video":["src","poster"],"source":"src","img":"src","image":"xlink:href"},"buble":{"transforms":{}}}!E:\hello-vue-cli\node_modules\vue-loader\lib\selector.js?type=template&index=0!E:\hello-vue-cli\src\App.vue |
| 7 | E:\hello-vue-cli\node_modules\url-loader\index.js??ref--2!E:\hello-vue-cli\src\assets\logo.png |
| 8 | E:\hello-vue-cli\node_modules\vue-loader\index.js??ref--0!E:\hello-vue-cli\src\components\HelloWorld.vue |
| 9 | E:\hello-vue-cli\node_modules\extract-text-webpack-plugin\dist\loader.js?{"omit":1,"remove":true}!E:\hello-vue-cli\node_modules\vue-style-loader\index.js!E:\hello-vue-cli\node_modules\css-loader\index.js?{"sourceMap":true}!E:\hello-vue-cli\node_modules\vue-loader\lib\style-compiler\index.js?{"vue":true,"id":"data-v-d8ec41bc","scoped":true,"hasInlineConfig":false}!E:\hello-vue-cli\node_modules\vue-loader\lib\selector.js?type=styles&index=0!E:\hello-vue-cli\src\components\HelloWorld.vue |
| 10 | E:\hello-vue-cli\node_modules\babel-loader\lib\index.js!E:\hello-vue-cli\node_modules\vue-loader\lib\selector.js?type=script&index=0!E:\hello-vue-cli\src\components\HelloWorld.vue |
| 11 | E:\hello-vue-cli\node_modules\vue-loader\lib\template-compiler\index.js?{"id":"data-v-d8ec41bc","hasScoped":true,"transformToRequire":{"video":["src","poster"],"source":"src","img":"src","image":"xlink:href"},"buble":{"transforms":{}}}!E:\hello-vue-cli\node_modules\vue-loader\lib\selector.js?type=template&index=0!E:\hello-vue-cli\src\components\HelloWorld.vue |
| 12 | E:\hello-vue-cli\node_modules\webpack\buildin\global.js |
图19 流程图2①步骤执行后的this.chunks中的13个module
3.2.1.4 this.chunks(2)#
如流程图2②,在函数"optimize-chunks-basic"、"optimize-chunks"和"optimize-chunks-advanced"执行完后,将this.chunks(1)中的13个module分到app、vendor和manifest等3个chunk中,如图20。其中,app的chunk下有10个module(如图21),vendor的chunk下有3个module,manifest的chunk下有0个module,它们与this.chunks(1)中module的对应关系如图22所示。
图20 流程图2①步骤执行后的this.chunks
图21 流程图2①步骤执行后的this.chunks
| Chunk.name | 集合大小:对应this.chunks(1)中_module元素的序号 |
|---|---|
| app | set(10): 2,3,5-11 |
| vendor | set(3): 1,4,12 |
| manifest | set(0) |
图22 流程图2①步骤执行后的this.chunks与执行前的this.chunks的对比
3.2.1.5 this.chunks(3)#
如流程图2③,在函数"optimize-chunk-modules-basic"、"optimize-chunk-modules"和"optimize-chunk-modules-advanced"执行完后,3.2.1.4节this.chunks(2)中app的chunk下生成3个NormalModule和1个ConcatenatedModule,如图23和图24。ConcatenatedModule下的dependencies个数与3.2.1.2节this.preparedchunks中的dependencies总数一致,为40个,如图25。ConcatenatedModule翻译为级联模块,在3.2.1.6节中,它将根据代码生成的module捆绑到app.js(app.[chunkhash].js的简写)的ConcatSource中,ConcatSource中会引用vendor.js(vendor.[chunkhash].js的简写)和manifest.js(manifest.[chunkhash].js的简写)的ConcatSource中的模块。所以级联模块下的dependencies是所有的dependencies。
图23 流程图2③步骤执行后的this.chunks
图24 流程图2③步骤执行后的this.chunks
图25 流程图2③步骤执行后的this.chunks
图26 流程图2③步骤执行后的this.chunks
ConcatenatedModule下索引为31的dependency的importDependency.request为"!!babel-loader!../node_modules/vue-loader/lib/selector?type=script&index=0!./App.vue",对应的importDependency.module._source._value的值如Demo17,Demo17中引入的“./components/HelloWorld”对应的是ConcatenatedModule下索引为19的dependency,如图26。索引为19的dependency的importDependency.module._source._value信息如Demo18。在第3.2.1.6节和第3.2.1.7节中将会跟踪这2个dependency值的变化。
import HelloWorld from './components/HelloWorld';
export default {
name: 'App',
components: {
HelloWorld: HelloWorld
},
methods: {
handleClick: function handleClick() {
alert(1);
}
}
};
Demo17 ConcatenatedModule下索引为31的dependency的importDependency.module._source._value值
function injectStyle (ssrContext) {
require("!!../../node_modules/extract-text-webpack-plugin/dist/loader.js?{\"omit\":1,\"remove\":true}!vue-style-loader!css-loader?{\"sourceMap\":true}!../../node_modules/vue-loader/lib/style-compiler/index?{\"vue\":true,\"id\":\"data-v-d8ec41bc\",\"scoped\":true,\"hasInlineConfig\":false}!../../node_modules/vue-loader/lib/selector?type=styles&index=0!./HelloWorld.vue")
}
var normalizeComponent = require("!../../node_modules/vue-loader/lib/component-normalizer")
/* script */
export * from "!!babel-loader!../../node_modules/vue-loader/lib/selector?type=script&index=0!./HelloWorld.vue"
import __vue_script__ from "!!babel-loader!../../node_modules/vue-loader/lib/selector?type=script&index=0!./HelloWorld.vue"
/* template */
import __vue_template__ from "!!../../node_modules/vue-loader/lib/template-compiler/index?{\"id\":\"data-v-d8ec41bc\",\"hasScoped\":true,\"transformToRequire\":{\"video\":[\"src\",\"poster\"],\"source\":\"src\",\"img\":\"src\",\"image\":\"xlink:href\"},\"buble\":{\"transforms\":{}}}!../../node_modules/vue-loader/lib/selector?type=template&index=0!./HelloWorld.vue"
/* template functional */
var __vue_template_functional__ = false
/* styles */
var __vue_styles__ = injectStyle
/* scopeId */
var __vue_scopeId__ = "data-v-d8ec41bc"
/* moduleIdentifier (server only) */
var __vue_module_identifier__ = null
var Component = normalizeComponent(
__vue_script__,
__vue_template__,
__vue_template_functional__,
__vue_styles__,
__vue_scopeId__,
__vue_module_identifier__
)
export default Component.exports
Demo18 ConcatenatedModule下索引为19的dependency的importDependency.module._source._value值
3.2.1.6 this.assets(1)#
如流程图2④,在compilation.createChunkAssets时,由app、manifest和vendor等chunk分别生成ConcatSource,并添加到this.assets中。this.assets的值如图27所示。app的chunk进行模块捆绑后,生成的this.assets下的键为app.js的值CachedSource的_source.children是一个长度为42的数组,如图28。数组中索引为25的元素如图29,它的注释是索引为24的元素// CONCATENATED MODULE: ./node_modules/babel-loader/lib!./node_modules/vue-loader/lib/selector.js?type=script&index=0!./src/App.vue。
图27 流程图2④步骤执行后的this.assets
图28 键为app.js的值CachedSource的_source.children
图29 _source.children数组中索引为25的元素
图30 Demo19中__WEBPACK_MODULE_REFERENCE__3_64656661756c74__表示引入的是数组中的第3个Concatenated Module
app.js下第25个元素的值如Demo19,其中__WEBPACK_MODULE_REFERENCE__3_64656661756c74__表示引入的是app.js中的第3个Concatenated Module,如图30。相比于3.2.1.5节ConcatenatedModule下的第31个元素,app.js下的第25个元素及其引入的模块都被打包进了同一个ConcatSource中,模块引用由原先的文件间通过import引用变成了ConcatSource内通过变量引用。
第3个Concatenated Module的值如Demo20。它对应于3.2.1.5节中ConcatenatedModule下第19个元素。相比而言,第3.2.1.5节中的var normalizeComponent = require("!../../node_modules/vue-loader/lib/component-normalizer")变成了var normalizeComponent = __webpack_require__("VU/8"),对component-normalizer模块的引用由模块间的require引用变成了同一ConcatSource内的__webpack_require__引用。__webpack_require__的参数VU/8是模块component-normalizer的ID,该模块位于vendor.js中,后续流程中app.js和vendor.js会被同一个index.html通过标签引入,所以对component-normalizer的引用可以看成是同一文件内的引用。Demo20中的其它细节可参考3.2.1.8节打包输出的完整文件。
/* harmony default export */ var __WEBPACK_MODULE_DEFAULT_EXPORT__ = ({
name: 'App',
components: {
HelloWorld: __WEBPACK_MODULE_REFERENCE__3_64656661756c74__
},
methods: {
handleClick: function handleClick() {
alert(1);
}
}
});
Demo19 _source.children数组中索引为25的元素
function injectStyle (ssrContext) {
__webpack_require__("1uuo")
}
var normalizeComponent = __webpack_require__("VU/8")
/* script */
/* template */
/* template functional */
var __vue_template_functional__ = false
/* styles */
var __vue_styles__ = injectStyle
/* scopeId */
var __vue_scopeId__ = "data-v-d8ec41bc"
/* moduleIdentifier (server only) */
var __vue_module_identifier__ = null
var Component = normalizeComponent(
__WEBPACK_MODULE_REFERENCE__1_64656661756c74__,
__WEBPACK_MODULE_REFERENCE__2_64656661756c74__,
__vue_template_functional__,
__vue_styles__,
__vue_scopeId__,
__vue_module_identifier__
)
/* harmony default export */ var __WEBPACK_MODULE_DEFAULT_EXPORT__ = (Component.exports);
Demo20 _source.children数组中索引为23的元素(第3个Concatenated Module)
3.2.1.7 this.assets(2)#
如流程图2⑤,在函数after-optimize-chunk-assets执行后,compilation.assets的值如图31所示。图31中除了app.js、manifest.js和vendor.js,还生成了对应的.map文件,.map文件是用于源代码调试的(详见5.1节)。如图32,3.2.1.6节中ConcatSource下的元素被合并到了同一个RawSource中,RawSource的值如Demo21所示,其中模块的变量名称相比于3.2.1.6节有修改,比如helloworld模块的变量名称由__WEBPACK_MODULE_REFERENCE__3_64656661756c74__修改为src_components_HelloWorld。
图31 流程图2⑤步骤执行后的this.assets
图32 ConcatSource下的元素被合并到了同一个RawSource中
/* harmony default export */ var App = ({
name: 'App',
components: {
HelloWorld: src_components_HelloWorld
},
methods: {
handleClick: function handleClick() {
alert(1);
}
}
});
function injectStyle (ssrContext) {
__webpack_require__("1uuo")
}
var normalizeComponent = __webpack_require__("VU/8")
/* template functional */
var __vue_template_functional__ = false
/* styles */
var __vue_styles__ = injectStyle
/* scopeId */
var __vue_scopeId__ = "data-v-d8ec41bc"
/* moduleIdentifier (server only) */
var __vue_module_identifier__ = null
var Component = normalizeComponent(
HelloWorld,
components_HelloWorld,
__vue_template_functional__,
__vue_styles__,
__vue_scopeId__,
__vue_module_identifier__
)
/* harmony default export */ var src_components_HelloWorld = (Component.exports);
Demo21 app.js下的RawSource的值
3.2.1.8 targetPath(根据outputPath生成)#
如流程图2⑥,打包后文件会输出到targetPath(根据outputPath生成)下。如图33,打包后文件app.js输出目录targetPath为/dist/static/js。文件的输出目录是在配置文件中配置的,详见5.6节。打包完成后查看项目的目录dist/static/js,目录下包括app.js、manifest.js和vendor.js,如图34。如果未配置html-webpack-plugin,则需要在输出目录下手动添加如Demo22的index.html。将目录下的文件部署到服务器上,可以正常访问。当index.html加载时,会依次加载manifest.js、vendor.js和app.js文件。如Demo23,app.js文件以调用函数webpackJsonp开头,该函数是manifest.js文件中的。webpackJsonp函数执行时会调用第2个参数中”NHnr“属性下的匿名函数,该匿名函数的最后通过vue_esm创建了vue实例,其中vue_esm是通过函数__webpack_require__输入的模块”7+uW“是vendor.js中的。可以在目标目录下app.js、manifest.js和vendor.js中查看更多细节。
图33 流程图2⑥步骤中的targetPath
图34 打包完成后的目录dist/static/js
html><html><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>myprojecttitle><link href=/hello/static/css/app.30790115300ab27614ce176899523b62.css rel=stylesheet>head><body><div id=app>div><script type=text/javascript src=/hello/static/js/manifest.16c23ba6bc6bb0b6d86e.js>script><script type=text/javascript src=/hello/static/js/vendor.a174ea6c2bddc077ea39.js>script><script type=text/javascript src=/hello/static/js/app.fb1b7cbbeb63fa1c2bfb.js>script>body>html>
Demo22 手动配置index.html
webpackJsonp([10],{
"1uuo": (function(module, exports) {...},
"7Otq": (function(module, exports) {...},
"NHnr": (function(module, __webpack_exports__, __webpack_require__) {
// EXTERNAL MODULE: ./node_modules/vue/dist/vue.esm.js
var vue_esm = __webpack_require__("7+uW");
new vue_esm["a" /* default */]({
el: '#app',
components: { App: src_App },
template: ' '
});
})
},["NHnr"]
}
Demo23 目标目录下的文件app.js
3.2.2 loader#
webpack打包时,会从入口文件开始构建依赖关系图,构建依赖关系图时会根据dependency生成module。如3.2.1.2节,由dependency生成module通常有3个步骤,一是根据源文件(resource)扩展名过滤出需要的loader;如果request中已包含前置loader,则使用前置loader;二是按元素索引从小到大依次执行loaders中的pitchingLoaders,按元素索引从大到小依次执行loaders中的NormalLoader,对源文件进行预处理;三是解析经过loader预处理后的代码,生成module及module下的dependencies。loader允许在根据dependency生成module时,对源文件进行预处理。可以在配置文件中配置loader,比如Demo24中,在rules分别配置了vue-loader、babel-loader和url-loader等3个loader。配置好loader后,webpack打包时,会根据文件扩展名获取需要的loader,loader会对源文件进行预处理。下面分别介绍vue-loader、babel-loader和url-loader是如何预处理的。
module.exports = {
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
}
}
Demo24 配置文件webpack.base.conf.js
3.2.2.1 vue-loader#
vue-loader会加载和编译vue组件。如3.2.1.2节,在由request为"./App"的dependency生成module时,源文件为E:\hello-vue-cli\src\App.vue,如Demo14;loader为E:\hello-vue-cli\node_modules\vue-loader\index.js??ref--0;loader预处理后的代码如Demo15所示。
如3.2.1.2节,rawRequest为"./App"的module下有13个dependencies。其中,索引为1的dependecy比较特殊,它的request为!!../node_modules/extract-text-webpack-plugin/dist/loader.js?{"omit":1,"remove":true}!vue-style-loader!css-loader?{"sourceMap":true}!../node_modules/vue-loader/lib/style-compiler/index?{"vue":true,"id":"data-v-6cbc4d12","scoped":false,"hasInlineConfig":false}!../node_modules/vue-loader/lib/selector?type=styles&index=0!./App.vue;第一个loader是插件extract-text-webpack-plugin下的函数。在processModuleDependecy中会为该dependency创建和构建module,loader预处理生成的代码如Demo25,这是由于没有配置extract-text-webpack-plugin导致的,未配置则Demo26中的this[NS]为undefined。如果配置了extract-text-webpack-plugin,则会注册函数“normal-module-loader”,函数中为loaderContext[NS]赋值,如Demo27。该函数在流程图5的doBuild方法中调用this.createLoaderContext时执行,在执行后续../node_modules/extract-text-webpack-plugin/dist/loader.js中的pitch函数(如Demo26)时,this[NS]有值,不再抛出异常。此时,打包生成的目标目录dist下,css文件将作为独立的文件,如图35。
throw new Error("Module build failed: Error: \"extract-text-webpack-plugin\" loader is used without the corresponding plugin, refer to https://github.com/webpack/extract-text-webpack-plugin for the usage example\n at Object.pitch (E:\\history_bak\\code\\32_mycode_hundsun\\08_code\\03_hello-vue-cli\\hello-vue-cli\\node_modules\\extract-text-webpack-plugin\\dist\\loader.js:57:11)");
Demo25 loader预处理生成的代码
//文件所属目录: hello-vue-cli\node_modules\extract-text-webpack-plugin\dist\loader.js
function pitch(request) {
var _this = this;
var query = _loaderUtils2.default.getOptions(this) || {};
var loaders = this.loaders.slice(this.loaderIndex + 1);
this.addDependency(this.resourcePath);
// We already in child compiler, return empty bundle
if (this[NS] === undefined) {
// eslint-disable-line no-undefined
throw new Error('"extract-text-webpack-plugin" loader is used without the corresponding plugin, ' + 'refer to https://github.com/webpack/extract-text-webpack-plugin for the usage example');
}
}
Demo26 extract-text-webpack-plugin模块中文件dist\loader.js
//文件所属目录: hello-vue-cli\node_modules\extract-text-webpack-plugin\dist\index.js
var ExtractTextPlugin = function () {
_createClass(ExtractTextPlugin, [{
key: 'apply',
value: function apply(compiler) {
var _this3 = this;
var options = this.options;
compiler.plugin('this-compilation', function (compilation) {
var extractCompilation = new _ExtractTextPluginCompilation2.default();
compilation.plugin('normal-module-loader', function (loaderContext, module) {
//为loaderContext[NS]赋值
loaderContext[NS] = function (content, opt) {
if (options.disable) {
return false;
}
if (!Array.isArray(content) && content != null) {
throw new Error(`Exported value was not extracted as an array: ${JSON.stringify(content)}`);
}
module[NS] = {
content,
options: opt || {}
};
return options.allChunks || module[`${NS}/extract`]; // eslint-disable-line no-path-concat
};
});
}
},...]
}
Demo27 extract-text-webpack-plugin模块中文件dist\index.js
图35 打包生成的目标目录下,css文件将作为独立的文件
如果不想将css打包成单独的css文件,就不需要配置extract-text-webpack-plugin。此时,vue-loader中也应删除cssLoader的相关配置。cssLoader的相关配置删除后rawRequest为"./App"的module下索引为1的dependecy,在创建和构建module时,loader预处理生成的代码如Demo28所示;而不会生成如Demo25所示的错误信息。
/ style-loader: Adds some css to the DOM by adding a 
































