写在前面:
本文是webpack的一个学习笔记,涉及webpack打包流程、plugin、loader的编写、以及实现一个简易版的webpack。
欢迎交流&有任何问题请大佬斧正
参考:
《深入浅出webpack》(写的很好,但是是基于webpack3+,tapable1.0以前的,跟最新的有些区别,但原理流程写的很清楚)
webpack原理与实战
webpack详解
本文基于webpack4+
webpack是一个打包模块化js的工具,可以通过loader转换文件,通过plugin扩展功能。
它本身结构精巧,基于tapable的插件架构,扩展性强,众多的loader或者plugin让webpack显得复杂。
一、webpack核心概念
一个常见的webpack配置长这样:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: "./app/entry", // string | object | array
// Webpack打包的入口
output: { // 定义webpack如何输出的选项
path: path.resolve(__dirname, "dist"), // string
// 所有输出文件的目标路径
filename: "[chunkhash].js", // string
// 「入口(entry chunk)」文件命名模版
publicPath: "/assets/", // string
// 构建文件的输出目录
/* 其它高级配置 */
},
module: { // 模块相关配置
rules: [ // 配置模块loaders,解析规则
{
test: /\.jsx?$/, // RegExp | string
include: [ // 和test一样,必须匹配选项
path.resolve(__dirname, "app")
],
exclude: [ // 必不匹配选项(优先级高于test和include)
path.resolve(__dirname, "app/demo-files")
],
loader: "babel-loader", // 模块上下文解析
options: { // loader的可选项
presets: ["es2015"]
},
},
},
resolve: { // 解析模块的可选项
modules: [ // 模块的查找目录
"node_modules",
path.resolve(__dirname, "app")
],
extensions: [".js", ".json", ".jsx", ".css"], // 用到的文件的扩展
alias: { // 模块别名列表
"module": "new-module"
},
},
devtool: "source-map", // enum
// 为浏览器开发者工具添加元数据增强调试
plugins: [ // 附加插件列表
new HtmlWebpackPlugin({template: './src/index.html'})
],
}
- Entry:指定webpack开始构建的入口模块,从该模块开始构建并计算出直接或间接依赖的模块或者库
- Output:输出的文件名以及输出的目录
- Loaders:文件转换器。Loaders将各类型的文件处理成webpack能够处理的模块,例如把es6转换为es5,scss转换为css
- Plugins:用于扩展webpack的功能,在webpack构建生命周期的节点上加入扩展hook为webpack加入功能。
- Chunk:coding split的产物,我们可以对一些代码打包成一个单独的chunk,比如某些公共模块,去重,更好的利用缓存。或者按需加载某些功能模块,优化加载时间。
二、webpack构建流程
从启动webpack构建到输出结果经历了一系列过程,它们是:
- 解析webpack配置参数,合并从shell传入和webpack.config.js文件里配置的参数,生产最后的配置结果。
- 注册所有配置的插件,好让插件监听webpack构建生命周期的事件节点,以做出对应的反应。
- 从配置的entry入口文件开始解析文件构建AST语法树,找出每个文件所依赖的文件,递归下去。
- 在解析文件递归的过程中根据文件类型和loader配置找出合适的loader用来对文件进行转换。
- 递归完后得到每个文件的最终结果,根据entry配置生成代码块chunk。
- 输出所有chunk到文件系统。
需要注意的是,在构建生命周期中有一系列插件在合适的时机做了合适的事情,比如UglifyJsPlugin会在loader转换递归完后对结果再使用UglifyJs压缩覆盖之前的结果。
三、webpack安装
如果你使用 webpack 4+ 版本,你还需要安装 CLI。
mkdir webpack-demo && cd webpack-demo
npm init -y
npm install webpack webpack-cli --save-dev
新建一个webpack.config.js文件
配置entry、output等必要内容
新建src文件夹,里面新建index.js文件
四、loader
对一个个单独的文件进行转换
常用loader:
https://webpack.docschina.org/loaders/
- babel-loader把es6转换成es5
- file-loader把文件替换成对应的URL
- raw-loader注入文本文件内容到代码里去
写一个loader
写一个将js文件中的mll都替代成另一个词的loader
可以在webpack.config.js同级新建一个文件夹loaders
里面新建一个文件replaceLoader.js
// loader就是一个函数
// 不能写成箭头函数
// 因为要用到this指向
// source:引入文件的内容
module.exports = function(source) {
return source.replace('mll', 'mll cool')
}
写一个loader其实很简单,将源代码处理后return就可以了
webpack中引入:
webpack.config.js
module: {
rules: [{
test: /\.js/,
use: [{
loader: path.resolve(__dirname, './loaders/replaceLoader.js')
}
]
}]
}
引入loader的位置
package.json
"scripts": {
"build": "webpack"
},
执行npm run build
打包的时候就会把js文件里的mll替换成mll cool
1.options传参
webpack.config.js
module: {
rules: [{
test: /\.js/,
use: [{
loader: path.resolve(__dirname, './loaders/replaceLoader.js'),
options: {
name: 'wasabi'
}
}
]
}]
}
loaders/replaceLoader.js
module.exports = function(source) {
console.log(this.query)
return source.replace('mll', 'mll cool')
}
在loader中,this.query能取到options中的值
打印出来的结果为
{ name: 'wasabi' }
因此
module.exports = function(source) {
return source.replace('mll', this.query.name)
}
可以通过外部传options来改变替换的内容
this.query的官方说法:
https://www.webpackjs.com/api/loaders/#this-query
如果这个 loader 配置了 options 对象的话,this.query 就指向这个 option 对象。
如果 loader 中没有 options,而是以 query 字符串作为参数调用时,this.query 就是一个以 ? 开头的字符串。
options 已取代 query,所以此属性废弃。使用 loader-utils 中的 getOptions 方法来提取给定 loader 的 option。
loader-utils
推荐使用loader-utils
npm install loader-utils --save-dev
举一个和callback一起使用的例子
https://www.webpackjs.com/api/loaders/#this-callback
this.callback(
err: Error | null,
content: string | Buffer,
sourceMap?: SourceMap,
meta?: any
);
module.exports = function(source) {
const options = loaderUtils.getOptions(this)
const result = source.replace('mll', options.name)
this.callback(null, result) // 等价于return
// this.callback(null, result, source, meta)
}
this.callback就相当于一个return的操作
但比return好的是,能传出去更多内容,例如源代码等等
2.异步操作
若loader中有异步操作
const loaderUtils = require('loader-utils')
module.exports = function(source) {
const options = loaderUtils.getOptions(this)
setTimeout(()=>{
const result = source.replace('mll', options.name)
return result
}, 1000)
}
因为一开始调用loader的时候没有return东西,因此会报错
Error: Final loader (./loaders/replaceLoader.js) didn't return a Buffer or String
解决:
this.async()
const loaderUtils = require('loader-utils')
module.exports = function(source) {
const options = loaderUtils.getOptions(this)
const callback = this.async()
setTimeout(()=>{
const result = source.replace('mll', options.name)
callback(null, result)
}, 5000)
}
然后打包,就能看到,打包时间5000+ms
Hash: aa999e77339b23001859
Version: webpack 4.39.3
Time: 5089ms
3.使用多个loader
loader的使用顺序是从下到上,从右到左
use: [{
loader: path.resolve(__dirname, './loaders/replaceLoader.js'),
},{
loader: path.resolve(__dirname, './loaders/replaceLoaderAsync.js'),
options: {
name: 'wasabi'
}
}
]
因此是先做replaceLoaderAsync,replaceLoaderAsync处理后的代码再扔进replaceLoader做处理
每次写
path.resolve(__dirname, './loaders/replaceLoaderAsync.js'),
很麻烦,有什么简化方法吗?
可以这么写:
resolveLoader: {
modules: ['node_modules', './loaders']
},
module: {
rules: [{
test: /\.js/,
use: [{
loader: 'replaceLoader'
},{
loader: 'replaceLoaderAsync',
options: {
name: 'wasabi'
}
}
]
}]
}
可以用resolveLoader
表示loader先在node_modules中找,找不到的话在./loaders下找
4.loader用途
自己开发loader一般可用在哪里呢?
对源代码做一些包装
例如:
- 网页有中文版和英文版,可以最开始写的时候弄一个占位符,在打包的时候通过loader切换??
- 代码做异常监控
在loader中对source中的function做try catch的包装。这样就可以不对业务源代码做改动了
五、plugin
常用plugin:https://webpack.docschina.org/plugins/
plugin编写
plugin 可以通过一些 hook 函数来拦截 webpack 的执行,甚至你可以运行一个子编译器和 loader 串联
a).Compilation、compiler
plugin 插件其实是一个含有 apply 方法的 class,而 apply 方法的参数就是 compiler 对象,compiler 对象里有各种钩子,这些钩子分别会在 webpack 的运行过程中触发,而实现这些钩子的核心是 tapable ,这个 tapable 还算好理解,可以把它看做是一个更高级的 发布-订阅。
总结:
- Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;
- Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等,代表了一次单一的版本构建和生成资源。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。
- Compiler 和 Compilation 的区别在于:Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。
可以通过 compilation 的 assets 对象来编写新的文件,或是修改已经创建的文件。
b).新建一个plugins
webpack.config.js同级下新建一个plugins文件夹
里面新建一个copyright-webpack-plugin.js文件
在原型上定义apply方法
// loader是个函数,plugin是个类
class CopyrightWebpackPlugin{
constructor(options) {
console.log('use plugin')
console.log(options)
}
// 调插件时会调用apply方法
// compiler 可以理解为webpack实例,存储了webpack各种各样的内容,打包的过程
apply(compiler) {
}
}
module.exports = CopyrightWebpackPlugin
webpack.config.js
const CopyrightWebpackPlugin = require('./plugins/copyright-webpack-plugin')
module.exports = {
.....
plugins: [
new CopyrightWebpackPlugin({
name: 'mll'
})
]
}
在插件的constructor中可以拿到向插件中传的值
因此打印出来
use plugin
{name: 'mll'}
c).hooks
tapable
tapable 这个小型 library 是 webpack 的一个核心工具,但也可用于其他地方,以提供类似的插件接口。webpack 中许多对象扩展自 Tapable 类。这个类暴露 tap, tapAsync 和 tapPromise 方法,可以使用这些方法,注入自定义的构建步骤,这些步骤将在整个编译过程中不同时机触发。
webpack中最核心的负责编译的Compiler和负责创建bundles的Compilation都是Tapable的实例。
webpack3及其以前使用的是Tapable1.0之前的版本,提供了包括:
- plugin(name:string, handler:function)注册插件到Tapable对象中
- apply(…pluginInstances: (AnyPlugin|function)[])调用插件的定义,将事件监听器注册到Tapable实例注册表- 中
- applyPlugins*(name:string, …)多种策略细致地控制事件的触发,包括applyPluginsAsync、- applyPluginsParallel等方法实现对事件触发的控制
之后使用的是1.0的Tapable
Tapable 1.0
暴露出很多的钩子,可以使用它们为插件创建钩子函数
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
1.0 Tapable使用方法:
https://github.com/webpack/tapable
https://juejin.im/post/5aa3d2056fb9a028c36868aa
Sync*类型的钩子:
- 注册在该钩子下面的插件的执行顺序都是顺序执行。
- 只能使用tap注册,不能使用tapPromise和tapAsync注册
对于Async*类型钩子:
- 支持tap、tapPromise、tapAsync注册
webpack中的使用
https://www.webpackjs.com/api/compiler-hooks/#emit
生命周期钩子函数,是由 compiler 暴露,可以通过如下方式访问:
compiler.hooks.someHook.tap(/* ... */);
取决于不同的钩子类型,也可以在某些钩子上访问 tapAsync 和 tapPromise。
- someHook为具体的钩子名称
- tap 同步、tapAsync 异步、tapPromise
异步可以使用 tap/tapAsync/tapPromise 方法触及。同步只能使用tap
以emit和compile的使用为例:
emit(异步hook):
AsyncSeriesHook
生成资源到 output 目录之前。
参数:compilation
compile(同步hook):
SyncHook
一个新的编译(compilation)创建之后,钩入(hook into) compiler。
写一个CopyrightWebpackPlugin
// loader是个函数,plugin是个类
class CopyrightWebpackPlugin{
constructor(options) {
console.log('use plugin')
console.log(options)
}
// 调插件时会调用apply方法
// compiler 可以理解为webpack实例,存储了webpack各种各样的内容,打包的过程
apply(compiler) {
// 同步,一个参数即可,不用传callback,后面也不用手动调用callback
compiler.hooks.compile.tap('CopyrightWebpackPlugin', (compilation) => {
console.log('compile')
})
// 异步时刻值(异步勾子)
// compilation中只存放了和这次打包有关的内容
// emit 时刻
// 异步是两个参数
compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin',(compilation, cb) => {
console.log(compilation.assets) // 打包生成的内容
// 在我们即将把代码放到dist目录之前,又增加了一个文件
compilation.assets['copyright.txt'] = {
// 内容
source: function() {
return 'copyright by mll'
},
size: function() {
return 16; // 内容长度
}
}
cb() // 一定要在最后调用一下cb
})
}
}
module.exports = CopyrightWebpackPlugin
会在dist目录下多生成一个copyright.txt文件,里面有内容copyright by mll
除此之外,还有done等等时间钩子。查阅文档即可https://www.webpackjs.com/api/compiler-hooks/
注意下同步和异步的不同写法:
同步:
compiler.hooks.compile.tap('CopyrightWebpackPlugin', (compilation) => {
console.log('compile')
})
同步只能使用tap触及
一个参数compilation
异步:
compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin',(compilation, cb) => {
console.log('test')
cb() // 一定要在最后调用一下cb
})
异步可以使用 tap/tapAsync/tapPromise 方法触及
两个参数(compilation, cb),第二个参数为回调函数cb,且最后一定要调用下这个回调函数cb()
(即在插件处理完任务时需要调用回调函数通知 Webpack,才会进入下一处理流程。 如果不执行 cb(),运行流程将会一直卡在这不往下执行 )
插件能够 钩入(hook) 到在每个编译(compilation)中触发的所有关键事件。在编译的每一步,插件都具备完全访问 compiler
对象的能力,如果情况合适,还可以访问当前 compilation
对象。
d).想知道compilation里面有些什么&& 调试 Webpack
由于 Webpack 运行在 Node.js 之上,调试 Webpack 就相对于调试 Node.js 程序。
在package.json中,加入
"scripts": {
"debug": "node --inspect --inspect-brk node_modules/webpack/bin/webpack.js",
},
node在运行webpack.js的时候,传递些node的参数
--inspect
开启node的调试工具
--inspect-brk
在运行webpack做调试的时候,在运行webpack命令执行的时候,在第一行打一个断点
运行npm run debug
打开浏览器控制台,发现控制台上会多一个绿色的node标志,点击标志,会打开一个node调试框DevTools-Node.js
就会到第一行的断点位置,这个文件就是webpack的打包过程
在我们写的plugin上也打上断点debugger
compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin',(compilation, cb) => {
debugger
...
})
就可以开始调试了
调试的时候就可以看到compilation里面有哪些内容了
1.可以鼠标放在compilation变量上
2.也可以compilation加到调试台右边的watch里,就能展开看了
六、编写一个简易版webpack
写一个bundler,了解webpack在打包时都做了什么
a).准备工作
src/index.js
import message from './message.js'
console.log(message)
src/message.js
import {word} from './word.js'
const message = `say ${word}`
export default message
src/word.js
export const word = 'hello'
根目录下新建一个bundler.js文件,就是我们要写的打包文件
b).一些依赖包的安装
1.想要命令行中console出来的东西有颜色:安装cli-highlight
npm install cli-highlight -g
用法:
node bundler.js | highlight
(在原本的命令后加上 | highlight)
这样打印出的内容就有颜色标识
2.把源代码解析为抽象语法树ast
https://www.babeljs.cn/docs/babel-parser
npm i @babel/parser --save
3.遍历ast
npm install @babel/traverse --save
https://www.babeljs.cn/docs/babel-core
npm i @babel/core --save
5.es6转es5
npm i @babel/preset-env --save
c).编写bundler.js
1.对入口文件进行分析
首先对入口文件做一个分析:
bundler.js:
// 读取入口文件,分析入口文件里的代码
const fs = require('fs')
const path = require('path')
const paser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
const moduleaAnalyser = (filename) => {
const content = fs.readFileSync(filename, 'utf-8')
const ast = paser.parse(content, {
sourceType: 'module'
}) // 抽象语法树 ast 是一个js对象
const dependencies = {} // 入口文件的依赖
// 遍历抽象语法树
traverse(ast, {
// 如果包含import引入语句,就会走这个函数
ImportDeclaration({node}) {
// console.log(node) // 能分析出源代码中有多少依赖
const dirname = path.dirname(filename) // 拿到filename的文件夹路径
// console.log(dirname)
const newFile = './' + path.join(dirname, node.source.value) // node.source.value为相对路径,这样可以拼成绝对路径
dependencies[node.source.value] = newFile
// console.log(dependencies) //{ './message.js': './src/message.js' } key为相对路径,value为绝对路径
}
})
// 将ast转换为浏览器可以使用的代码
const {code} = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]// es6转为es5
})
// console.log(code)
return {
filename,
dependencies,
code
}
// console.log(dependencies)
// console.log(ast.program.body)
}
const moduleInfo = moduleaAnalyser('./src/index.js')
console.log(moduleInfo)
执行node bundler.js | highlight ,会打印出来如下内容:
{ filename: './src/index.js',
dependencies: { './message.js': './src/message.js' },
code:
'"use strict";\n\nvar _message = _interopRequireDefault(require("./message.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nconsole.log(_message["default"]);' }
filename:分析的模块路径(文件路径)
dependencies:该模块依赖的其他模块(文件路径)(key为相对路径,value为绝对路径,这样写是为了后面使用方便)
code:该模块转换后的代码
2.从入口文件开始,对所有其依赖的模块做相同的分析
// 读取入口文件,分析入口文件里的代码
const fs = require('fs')
const path = require('path')
const paser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
// 对模块进行分析
const moduleaAnalyser = (filename) => {
const content = fs.readFileSync(filename, 'utf-8')
const ast = paser.parse(content, {
sourceType: 'module'
}) // 抽象语法树 ast 是一个js对象
const dependencies = {} // 入口文件的依赖
// 遍历抽象语法树
traverse(ast, {
// 如果包含import引入语句,就会走这个函数
ImportDeclaration({node}) {
// console.log(node) // 能分析出源代码中有多少依赖
const dirname = path.dirname(filename) // 拿到filename的文件夹路径
// console.log(dirname)
const newFile = './' + path.join(dirname, node.source.value) // node.source.value为相对路径,这样可以拼成绝对路径
dependencies[node.source.value] = newFile
// console.log(dependencies) //{ './message.js': './src/message.js' } key为相对路径,value为绝对路径
}
})
// 将ast转换为浏览器可以使用的代码
const {code} = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]// es6转为es5
})
// console.log(code)
return {
filename,
dependencies,
code
}
// console.log(dependencies)
// console.log(ast.program.body)
}
// 整个项目的依赖关系,依赖图谱
// entry 项目的入口文件
const makeDependenciesGraph = (entry) => {
const entryModule = moduleaAnalyser(entry) // 对入口文件进行依赖分析
const graphArray = [entryModule]
for(let i = 0; i < graphArray.length; i++) {
const item = graphArray[i]
const {dependencies} = item
if(dependencies) {
for(let j in dependencies) {
graphArray.push(moduleaAnalyser(dependencies[j]))
}
}
}
console.log(graphArray)
// 为了后面代码打包比较方便,对它做一个结构上的转换
const graph = {}
graphArray.forEach(item => {
graph[item.filename] = {
dependencies: item.dependencies,
code: item.code
}
})
//console.log(graph)
return graph
}
// const moduleInfo = moduleaAnalyser('./src/index.js')
// console.log(moduleInfo)
const graphInfo = makeDependenciesGraph('./src/index.js')
console.log(graphInfo)
但是这里生成的代码还不能直接在浏览器上执行,因为浏览器上是没有require函数,exports对象的。
继续改造:
// 读取入口文件,分析入口文件里的代码
const fs = require('fs')
const path = require('path')
const paser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
// 对模块进行分析
const moduleaAnalyser = (filename) => {
const content = fs.readFileSync(filename, 'utf-8')
const ast = paser.parse(content, {
sourceType: 'module'
}) // 抽象语法树 ast 是一个js对象
const dependencies = {} // 入口文件的依赖
// 遍历抽象语法树
traverse(ast, {
// 如果包含import引入语句,就会走这个函数
ImportDeclaration({node}) {
// console.log(node) // 能分析出源代码中有多少依赖
const dirname = path.dirname(filename) // 拿到filename的文件夹路径
// console.log(dirname)
const newFile = './' + path.join(dirname, node.source.value) // node.source.value为相对路径,这样可以拼成绝对路径
dependencies[node.source.value] = newFile
// console.log(dependencies) //{ './message.js': './src/message.js' } key为相对路径,value为绝对路径
}
})
// 将ast转换为浏览器可以使用的代码
const {code} = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]// es6转为es5
})
// console.log(code)
return {
filename,
dependencies,
code
}
// console.log(dependencies)
// console.log(ast.program.body)
}
// 整个项目的依赖关系,依赖图谱
// entry 项目的入口文件
const makeDependenciesGraph = (entry) => {
const entryModule = moduleaAnalyser(entry) // 对入口文件进行依赖分析
const graphArray = [entryModule]
for(let i = 0; i < graphArray.length; i++) {
const item = graphArray[i]
const {dependencies} = item
if(dependencies) {
for(let j in dependencies) {
graphArray.push(moduleaAnalyser(dependencies[j]))
}
}
}
console.log(graphArray)
// 为了后面代码打包比较方便,对它做一个结构上的转换
const graph = {}
graphArray.forEach(item => {
graph[item.filename] = {
dependencies: item.dependencies,
code: item.code
}
})
//console.log(graph)
return graph
}
const generateCode = (entry) => {
const graph = JSON.stringify(makeDependenciesGraph(entry))
// 闭包 避免污染全局
// 返回的是个字符串,最后在浏览器中运行
// 浏览器上是没有require函数,exports对象的,所以代码直接放到浏览器上是不能直接运行的
// 因此我们要构造这两个东西
// eval(code) // 执行代码
// localRequire 相对路径转换绝对路径
return `
(function(graph){
function require(module) {
function localRequire(relativePath) {
return require(graph[module].dependencies[relativePath]);
}
var exports = {};
(function(require, exports, code){
eval(code)
})(localRequire, exports, graph[module].code);
return exports;
};
require('${entry}')
})(${graph});
`
}
// const moduleInfo = moduleaAnalyser('./src/index.js')
// console.log(moduleInfo)
const graphInfo = generateCode('./src/index.js')
console.log(graphInfo)
运行 node bundler.js | highlight 就会生成如下一段代码
(function(graph){
function require(module) {
function localRequire(relativePath) {
return require(graph[module].dependencies[relativePath]);
}
var exports = {};
(function(require, exports, code){
eval(code)
})(localRequire, exports, graph[module].code);
return exports;
};
require('./src/index.js')
})({"./src/index.js":{"dependencies":{"./message.js":"./src/message.js"},"code":"\"use strict\";\n\nvar _message = _interopRequireDefault(require(\"./message.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(_message[\"default\"]);"},"./src/message.js":{"dependencies":{"./word.js":"./src/word.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\n\nvar _word = require(\"./word.js\");\n\nvar message = \"say \".concat(_word.word);\nvar _default = message;\nexports[\"default\"] = _default;"},"./src/word.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.word = void 0;\nvar word = 'hello';\nexports.word = word;"}});
把这段代码复制到浏览器中,在console中执行,最后能输入say hello
这样,我们就把原本不能在浏览器端执行的代码转换为浏览器能执行的,webpack打包工具的原理也就是这样
七、生产环境和开发环境
生产环境和开发环境一般会配两个不同的webpack配置:
分开写配置文件就要涉及到使用命令执行不同的配置文件,我们可以使用npm的脚本命令,我们可以在package.json中找到scripts,添加如下命令
"build": "NODE_ENV=production webpack --config ./webpack.production.config.js --progress"
- NODE_ENV=production 就是将运行环境设置成生产环境
- webpack --config 就是运行webpack的配置文件
- ./webpack.production.config.js 是要运行的指定位置的文件,这个路径是相对根目录来说的
- --progress 是编译过程显示进程百分比的
开发环境一般不会配压缩代码、生成hash的插件,避免打包时间过长