1. 如何编写一个Loader
1.1 实现一个简单的Loader
我们开始写一个打包之后的文件,将js
代码中jie
这个字符串替换为world
的一个loader
,首先我们新建一个功能,使用npm init
,然后进行安装webpack
:npm install webpack webpack-cli --save-dev
,安装完之后,新建一个文件及src
以及loaders
,然后分别在对应的文件夹中新建index.js
以及replace.loaders.js
文件。
然后新建webpack
的配置文件webpack.config.js
,内容如下:在module
使用我们的loader
const path = require('path')
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
// 使用我们的loader
module: {
rules: [{
test: /\.js/,
use: [path.resolve(__dirname, './loaders/replace.Loader.js')]
}]
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
}
然后replace.loaders.js
文件内容:
module.exports = function(source) {
//source就是我们调用loader传进来的源码。
console.log(source)
return source.replace('jie', 'world');
}
- 这里不能使用箭头函数,是因为在该函数中,要使用this的指向,
webpack
在调用loader
的时候会进行this指向的变更。如果在定义的时候绑定this
,会出现问题
index.js
中的内容很简单:
console.log('hello jie');
就这样,一个简单的loader
就制作好了。在package.json
里面配置一个命令"build": "webpack"
进行打包;
1.2 Loader 中的参数传递
有时候我们需要给我们的loader
进行传递参数,可以修改 配置文件webpack.config.js
:这里配置loader
的use
属性也是一个对象,loader
就是我们配置的地址,opotion
就是我们需要传递的参数。
const path = require('path')
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
// 使用我们的loader
module: {
rules: [{
test: /\.js/,
use: [{
loader: path.resolve(__dirname, './loaders/replace.Loader.js'),
options: {
name: 'giser'
}
}]
}]
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
}
然后在loader
中,接收传进来的参数:
module.exports = function(source) {
//source就是我们调用loader传进来的源码。
console.log(source)
//接收传进来的参数
console.log(this.query);
return source.replace('jie', 'world');
}
可以在官网查看很多API
的用法:https://webpack.js.org/api/loaders/ ,在获取传进来的参数的时候,我们可以使用官方推荐的一个loader-utils
的模块进行获取参数,输入命令npm install loader-utils --save-dev
,然后在我们的loader
中进行获取参数:使用getOptions(this)
进行获取所有的参数。
const loaderUtils = require('loader-utils')
//这里不能使用箭头函数,是因为在该函数中,要使用this的指向,webpack在调用loader的时候会进行this指向的变更。如果在定义的时候绑定this,会出现问题
module.exports = function(source) {
//source就是我们调用loader传进来的源码。
console.log(source)
//接收传进来的参数
// console.log(this.query);
const options = loaderUtils.getOptions(this)
console.log(options.name)
return source.replace('jie', this.query.name);
}
1.3 Loader 中多个参数的返回
有时候我们想在我们的loader
中,返回很多参数,而现在的只是返回了我们处理后的源代码,this.callback
这个函数,可以帮助我们返回更多参数:
this.callback(
err: Error | null,
content: string | Buffer,
sourceMap?: SourceMap,
meta?: any
);
我们在我们的loader
中修改如下:
const loaderUtils = require('loader-utils')
//这里不能使用箭头函数,是因为在该函数中,要使用this的指向,webpack在调用loader的时候会进行this指向的变更。如果在定义的时候绑定this,会出现问题
module.exports = function(source) {
//source就是我们调用loader传进来的源码。
console.log(source)
//接收传进来的参数
// console.log(this.query);
const options = loaderUtils.getOptions(this)
console.log(options.name)
// return source.replace('jie', this.query.name);
const result = source.replace('jie', this.query.name);
// 第一个参数为错误信息,第二个参数为要返回的内容,第三个参数为sourceMap,第四个参数为返回的其他信息
this.callback(null, result, source, meta)
}
而我们现在的代码中没有sourcemap
我们可以修改为:
const loaderUtils = require('loader-utils')
//这里不能使用箭头函数,是因为在该函数中,要使用this的指向,webpack在调用loader的时候会进行this指向的变更。如果在定义的时候绑定this,会出现问题
module.exports = function(source) {
//source就是我们调用loader传进来的源码。
console.log(source)
//接收传进来的参数
// console.log(this.query);
const options = loaderUtils.getOptions(this)
console.log(options.name)
// return source.replace('jie', this.query.name);
const result = source.replace('jie', this.query.name);
// 第一个参数为错误信息,第二个参数为要返回的内容,第三个参数为sourceMap,第四个参数为返回的其他信息
this.callback(null, result)
}
1.4 Loader 中处理异步请求
有时候我们需要在loader
中处理一些异步请求数据,下面我们用setTimeout
来模拟异步数据会获取:
const loaderUtils = require('loader-utils')
module.exports = function(source) {
console.log(source)
const options = loaderUtils.getOptions(this)
const callback = this.async();
setTimeout(() => {
const result = source.replace('jie', this.query.name);
callback(null, result)
}, 1000);
}
首先我们声明一个异步操作的函数const callback = this.async();
,然后在里面使用该函数,这里需要注意的是this.async
异步函数返回的结果也是调用了this.callback
这个函数,所以我们第一个参数如果没有错误信息就传递一个null
,该参数是必须要传递的。这样就实现了在loader
中处理异步请求。
- this.async
Tells the loader-runner that the loader intends to call back asynchronously. Returns this.callback.
1.5 多个 Loader 的使用
如果我们有多个loader
进行使用,跟之前的一样,直接在use
选项里加上我们需要使用的loader
:
// 使用我们的loader
module: {
rules: [{
test: /\.js/,
use: [
{
loader: path.resolve(__dirname, './loaders/replace.Loader.js'),
options: {
name: 'giser'
}
}, {
loader: path.resolve(__dirname, './loaders/replaceLoaderAsync.js'),
options: {
name: 'giser'
}
}
]
}]
},
我们会发现,如果每次都要加一个loader
进行使用的话,都需要写一次path.resolve(__dirname, './loaders/replaceLoaderAsync.js'),
这种东西,我们希望的是我们加载自己的loader
是跟安装其他第三方包一样,只写loader
名称就可以,如下:
// 使用我们的loader
module: {
rules: [{
test: /\.js/,
use: [
{
loader: 'replace.Loader',
options: {
name: 'giser'
}
}, {
loader: 'replaceLoaderAsync',
options: {
name: 'giser'
}
}
]
}]
},
如果这样,我们可以使用一个reaolveLoader
:resolveLoader
代码意思是,如果我们引用一个loader
,他会先去node_modules
中去找如果没有,就去loaders
的文件夹中去找。
const path = require('path')
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
resolveLoader: {
modules: ['node_modules', './loaders']
},
// 使用我们的loader
module: {
rules: [{
test: /\.js/,
use: [
{
loader: 'replace.Loader',
options: {
name: 'giser'
}
}, {
loader: 'replaceLoaderAsync',
options: {
name: 'giser'
}
}
]
}]
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
}
我们可以使用loader
做很多,比如我们一般在代码中加上try{}c atch{}
进行捕获异常,但是直接在业务代码中加上这些,会显得代码很乱,而且自己也要加很多这样的语句,很是麻烦,我们可以通过写一个loader
,进行帮我们做这些事,在这个loader
中,我们进行检测源码,如果有function
字符串,就对这个函数添加try{}c atch{}
进行捕获异常:
try {function () {
}catch(e)}
还有比如我们有一个网站,会打包输出一个中文版跟英文版本的,我们如果每一个都去修改代码里面标题这些,会很繁琐,我们前面说了,可以在loader
中进行传递参数,这样就会获取到全局变量,然后根据这个全局变量进行打包我们的代码,是中文还是英文版本的,我们在元源码中使用一个占位符,进行根据全局变量,来替换这个占位符,从而达到打包输出中文以及英文版本的:
我们的源码:{{title}}
然后在loader
中:
if(Node全局变量 === '中文') {
source.replace('{{title}}', '中文标题')
} else {
source.replace('{{title}}', '英文标题')
}
使用loader
可以进行对我们的源代码进行包装。方便我们进行处理一些多而繁琐的操作。
2. 如何编写一个 Plugin
首先要知道loader
与plugin
之间的关系:loader
是当我们进行打包我们的文件时,处理不同类型的文件,处理模块。plugiin
是在打包的时候具体时刻,进行处理事件,比如我们在每次打包之前清除dist
目录下的文件,就会使用clean-webpack-plugin
插件进行处理。对于webpack
的插件的核心机制或者说设计模式就是事件驱动以及发布模式;他是通过事件来驱动的。首先我们新建一个工程,类似上面的搭建loader
的工程,然后新建一个文件夹plugins
,里面新建一个copyright-webpack-plugins.js
我们的插件,一般来说,插件的命名都是*-webpack-plugin.js
这样子的,我们这个插件实现的一个功能就是,给我们每一个页面或者是脚本中添加一个版权的标识,copyright-webpack-plugins.js
内容如下:注意这里的插件声明方式,是通过class
声明的;
// 定义一个插件
class CopyrightWebpackPlugin {
constructor () {
}
apply (compiler) {
}
}
module.exports = CopyrightWebpackPlugin;
然后在webpack.config.js
中使用我们的插件:也正是因为我们前面插件声明是通过class
,所以这里需要使用new
关键字来进行实例化我们的插件。
const path = require('path');
//引入我们的插件
const CopyRightWebpackPlugin = require('./plugins/copyright-webpack-plugin');
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
// 使用我们的插件
plugins: [
new CopyRightWebpackPlugin()
],
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
}
2.1 插件中接收参数
我们在实例化插件的时候,可以传入参数:
// 使用我们的插件
plugins: [
new CopyRightWebpackPlugin({
name: 'jie'
})
],
插件的构造函数中会接收我们的插件:这里的options
就是我们传递的参数;
// 定义一个插件
class CopyrightWebpackPlugin {
constructor (options) {
console.log(options)
}
apply (compiler) {
}
}
module.exports = CopyrightWebpackPlugin;
我们现在想做一个就是在打包完成之后要放到dist
文件夹的时候,往dist
文件夹中增加一个copyright.txt
的文件:这里就需要使用apply
方法,这里的参数compiler
是一个webpack
实例,包含webapck
打包过程以及配置文件等等,这里有一个compiler.hooks
类似于vue
中的一些钩子函数,里面有很多时刻,可以查看官方文档:https://webpack.js.org/api/compiler-hooks/#afteremit
里面有很多,我们要实现的方法就是在emit
时刻执行,emit
时刻也就是在打包完成之后要放到dist
文件夹的时候。具体实现代码:
// 定义一个插件
class CopyrightWebpackPlugin {
// constructor (options) {
// console.log(options)
// }
apply (compiler) {
compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (complication, cb) => {
//complication存放这次打包的所有的配置内容,compiler是存放所有的配置内容,
// 打包内容中有哪些文件是放在complication.assets中的,所以我们只需要在complication.assets中
// 添加一个对象,塞入我们需要添加的文件。
complication.assets['copyright.txt'] = {
// 里面的内容
source: function() {
return 'copyright by jie'
},
// 文件大小,字符长度
size: function() {
return 16;
}
}
console.log('1111')
cb();
})
}
}
module.exports = CopyrightWebpackPlugin;
前面的时刻都是一步的时刻,也就是他返回的是一个AsyncSeriesHook
,同步的时刻跟异步时刻实现的方法是不一样的,比如compole
时候,代码如下:
compiler.hooks.compile.tap('CopyrightWebpackPlugin', (complication) => {
console.log('111')
})
我们有时候想知道complication
这个对象里面包含的一些属性,直接通过console.log()
的方式在控制台中输出时不太直观的,我们可以配置一个命令"debug": "node node_modules/webpack/bin/webpack.js"
通过node
的调试工具来进行查看。其实这个命令跟上面我们配置的直接执行webpack
效果是一样的,不过这个通过这种我们可以传递一些node
的参数,第一个参数inspect
是开启node
的调试工具,第二个参数inspect-brk
在运行webpack
做调试的时候,在第一行代码就打一个断点,运行命令之后,我们就在网页f12
会看到下面的东西:随便一个网页,
点击这个node
的图标按钮之后,会跳转到代码的调试,可以看到有断点:
或者是我们在代码中在需要调试的位置添加debugger
然后进行调试:
// 定义一个插件
class CopyrightWebpackPlugin {
// constructor (options) {
// console.log(options)
// }
apply (compiler) {
compiler.hooks.compile.tap('CopyrightWebpackPlugin', (complication) => {
console.log('111')
})
compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (complication, cb) => {
//complication存放这次打包的所有的配置内容,compiler是存放所有的配置内容,
// 打包内容中有哪些文件是放在complication.assets中的,所以我们只需要在complication.assets中
// 添加一个对象,塞入我们需要添加的文件。
debugger;
complication.assets['copyright.txt'] = {
// 里面的内容
source: function() {
return 'copyright by jie'
},
// 文件大小,字符长度
size: function() {
return 16;
}
}
console.log('1111')
cb();
})
}
}
module.exports = CopyrightWebpackPlugin;
以及我们可以在调试工具的Watch
里监听我们需要的监听的对象:
3. Bundler 源码编写 (模块分析)
我们实现一个类似于webpack
这样的打包工具,来逐渐分析webpack
实现打包的原理。首先新建一个bundle
文件夹,里面跟上面一样先初始化项目,然后新建dist
目录,目录结构如下:
对应的几个js
文件内容也特别简单:
index.js
:
import message from './message.js';
console.log(message);
message.js
:
// 后缀.js要写,我们的工具不支持后缀的缩写
import { word } from './word.js';
const message = `say ${word}`;
export default message;
word.js
:
export const word = 'hello';
就这样,可以看到我们的项目中,有es6
中的import
这些语法,直接运行在浏览器,肯定是不可以的,所以我们现在做的就是,写一个类似一些打包工具,进行将我们的代码处理成可以被浏览器识别的代码。在根目录新建一个bundler.js
:我们要做的是首先读取入口文件,然后分析入口文件的代码,
// nodeJS中的模块,用于获取文件信息
const fs = require('fs')
// 引入babel/parser用来分析我们的源代码
const parser = require('@babel/parser')
//filename为要分析的入口文件
const moduleAnalyser = (filename) => {
// 读取文件中的内容,第一个参数为文件地址,第二个参数为文件编码方式
const content = fs.readFileSync(filename, 'utf-8')
console.log(parser.parse(content, {
// 如果要对于es6语法的代码进行分析,需要传入一个sourceType选项,sourceType: 'module'
sourceType: 'module'
}));
console.log(content)
}
moduleAnalyser('./src/index.js')
可以安装一个工具
npm install cli-highlight -g
用于控制台输出代码高亮。运行时输入命令node bundler.js | highlight
安装一个插件npm install @babel/parser --save
用来帮助我们分析读取到的源代码。可以打开官网,查看具体的例子:https://babeljs.io/docs/en/babel-parser
,我们查看上面代码打印的内容,输入node bundler.js | highlight
,打印结果如下:
其实这是一个抽象语法树的表述方式,我们可以打印一下该对象的program.body
,如下:
[ Node {
//第一个节点是import语法声明
type: 'ImportDeclaration',
start: 0,
end: 35,
loc: SourceLocation { start: [Position], end: [Position] },
specifiers: [ [Node] ],
source:
Node {
type: 'StringLiteral',
start: 20,
end: 34,
loc: [SourceLocation],
extra: [Object],
value: './message.js' } },
Node {
//一个表达式的语句,
type: 'ExpressionStatement',
start: 37,
end: 58,
loc: SourceLocation { start: [Position], end: [Position] },
expression:
Node {
type: 'CallExpression',
start: 37,
end: 57,
loc: [SourceLocation],
callee: [Node],
arguments: [Array] } } ]
可以看到分析的抽象语法树,很好的将我们的js
代码转换成了js
对象。我们现在需要的是拿到我们代码中所有的依赖关系,也就是读取到import
的节点,然后去分析里面的内容,可以去循环这个对象的program.body
然后找到type = 'ImportDeclaration'
,但是是有点麻烦,我们可以借助一个工具,输入命令安装:npm install --save @babel/traverse
,然后我们使用,代码如下:
// nodeJS中的模块,用于获取文件信息
const fs = require('fs');
// 引入babel/parser用来分析我们的源代码
const parser = require('@babel/parser');
// 引入babel/traverse来帮助我们分析抽象语法树中 type = 'ImportDeclaration' 的节点,default是我们使用export default导出的内容,因为默认导出是export module
const traverse = require('@babel/traverse').default;
//filename为要分析的入口文件
const moduleAnalyser = (filename) => {
// 读取文件中的内容,第一个参数为文件地址,第二个参数为文件编码方式
const content = fs.readFileSync(filename, 'utf-8')
const ast = parser.parse(content, {
// 如果要对于es6语法的代码进行分析,需要传入一个sourceType选项,sourceType: 'module'
sourceType: 'module'
});
// 存放依赖的文件
const dependencies = []
// 第一个参数为抽象语法树类型的参数,第二个参数为需要查找的内容,
// 这里我们查找ImportDeclaration节点的内容,只要包含他,就会走这个函数,
traverse(ast, {
// 获取到该节点的node节点
ImportDeclaration({ node }) {
console.log(node)
}
})
}
moduleAnalyser('./src/index.js')
存放依赖的文件,查看打印的内容,我们可以看到source
中的value
存放着依赖的文件的地址
然后我们将节点中的node.source.value
值存放到依赖的文件,也就是dependencies
变量中:我们可以看到我们获取的地址是一个相对路径,相对于src
目录的,真正做打包的时候,我们希望我们获取的地址是一个相对路径,或者是相对于根目录的路径,我们可以利用nodeJS
中的path
模块,来解决这个问题,
// nodeJS中的模块,用于获取文件信息
const fs = require('fs');
// 引入babel/parser用来分析我们的源代码
const parser = require('@babel/parser');
// 引入babel/traverse来帮助我们分析抽象语法树中 type = 'ImportDeclaration' 的节点,default是我们使用export default导出的内容
const traverse = require('@babel/traverse').default;
// 引入nodeJS的核心模块 path
const path = require('path');
//filename为要分析的入口文件
const moduleAnalyser = (filename) => {
// 读取文件中的内容,第一个参数为文件地址,第二个参数为文件编码方式
const content = fs.readFileSync(filename, 'utf-8')
const ast = parser.parse(content, {
// 如果要对于es6语法的代码进行分析,需要传入一个sourceType选项,sourceType: 'module'
sourceType: 'module'
});
// 存放依赖的文件- 相对路径与绝对路径
const dependencies = {}
// 第一个参数为抽象语法树类型的参数,第二个参数为需要查找的内容,
// 这里我们查找ImportDeclaration节点的内容,只要包含他,就会走这个函数,
traverse(ast, {
// 获取到该节点的node节点
ImportDeclaration({ node }) {
// 获取到filename的路径 也就是主入口文件的路径 ./src
const dirname = path.dirname(filename);
// 将相对路径转换为绝对路径 ./src/message.js
const newFile = './'+path.join(dirname, node.source.value);
// 存储相对路径与绝对路径
dependencies[node.source.value] = newFile;
// dependencies.push(newFile);
}
})
return {
filename,
dependencies
}
}
moduleAnalyser('./src/index.js')
我们这个时候只是分析了代码中的import
的引入方式,我们要做的是把原始的代码打包编译之后能在浏览器上运行,所以我们需要借助一个工具:npm install @babel/core --save
对代码进行转换,他是babel
的一个核心模块,可以利用babel.transformFromAst
函数将抽象语法树转换为可以运行的代码,我们还利用babel/preset-env
来将es6
语法转换为es5
的语法:npm install @babel/preset-env --save
进行安装,实现代码如下:
// nodeJS中的模块,用于获取文件信息
const fs = require('fs');
// 引入babel/parser用来分析我们的源代码
const parser = require('@babel/parser');
// 引入babel/traverse来帮助我们分析抽象语法树中 type = 'ImportDeclaration' 的节点,default是我们使用export default导出的内容
const traverse = require('@babel/traverse').default;
// 引入nodeJS的核心模块 path
const path = require('path');
// 引入babel/core来准换我们的代码
const babel = require('@babel/core');
//filename为要分析的入口文件
const moduleAnalyser = (filename) => {
// 读取文件中的内容,第一个参数为文件地址,第二个参数为文件编码方式
const content = fs.readFileSync(filename, 'utf-8')
const ast = parser.parse(content, {
// 如果要对于es6语法的代码进行分析,需要传入一个sourceType选项,sourceType: 'module'
sourceType: 'module'
});
// 存放依赖的文件- 相对路径与绝对路径
const dependencies = {}
// 第一个参数为抽象语法树类型的参数,第二个参数为需要查找的内容,
// 这里我们查找ImportDeclaration节点的内容,只要包含他,就会走这个函数,
traverse(ast, {
// 获取到该节点的node节点
ImportDeclaration({ node }) {
// 获取到filename的路径 也就是主入口文件的路径 ./src
const dirname = path.dirname(filename);
// 将相对路径转换为绝对路径 ./src/message.js
const newFile = './'+path.join(dirname, node.source.value);
// 存储相对路径与绝对路径
dependencies[node.source.value] = newFile;
// dependencies.push(newFile);
}
});
// 借助babel的transformFromAst方法将抽象语法树转换为可以运行的代码。
// 第一个参数是一个抽象语法树,第二个参数是sourceCode,第三个参数是一些转换的Options
// 这里解析后的code 就是可以在浏览器运行的代码
const { code } = babel.transformFromAst(ast, null, {
// 插件的集合-将es6语法转换为es5
presets: ["@babel/preset-env"]
});
return {
filename,
dependencies,
code
}
}
const moduleInfo = moduleAnalyser('./src/index.js');
console.log(moduleInfo)
上面的代码是将我们的入口文件进行了分析,并转换成了可以在浏览器上运行的代码,接下来我们要实现将入口文件依赖的文件也进行分析,并转换为在浏览器上可以运行的代码,代码如下:
// nodeJS中的模块,用于获取文件信息
const fs = require('fs');
// 引入babel/parser用来分析我们的源代码
const parser = require('@babel/parser');
// 引入babel/traverse来帮助我们分析抽象语法树中 type = 'ImportDeclaration' 的节点,default是我们使用export default导出的内容
const traverse = require('@babel/traverse').default;
// 引入nodeJS的核心模块 path
const path = require('path');
// 引入babel/core来准换我们的代码
const babel = require('@babel/core');
//filename为要分析的入口文件
const moduleAnalyser = (filename) => {
// 读取文件中的内容,第一个参数为文件地址,第二个参数为文件编码方式
const content = fs.readFileSync(filename, 'utf-8')
const ast = parser.parse(content, {
// 如果要对于es6语法的代码进行分析,需要传入一个sourceType选项,sourceType: 'module'
sourceType: 'module'
});
// 存放依赖的文件- 相对路径与绝对路径
const dependencies = {}
// 第一个参数为抽象语法树类型的参数,第二个参数为需要查找的内容,
// 这里我们查找ImportDeclaration节点的内容,只要包含他,就会走这个函数,
traverse(ast, {
// 获取到该节点的node节点
ImportDeclaration({ node }) {
// 获取到filename的路径 也就是主入口文件的路径 ./src
const dirname = path.dirname(filename);
// 将相对路径转换为绝对路径 ./src/message.js
const newFile = './'+ path.join(dirname, node.source.value).replace('\\', '/');
// 存储相对路径与绝对路径
dependencies[node.source.value] = newFile;
// dependencies.push(newFile);
}
});
// 借助babel的transformFromAst方法将抽象语法树转换为可以运行的代码。
// 第一个参数是一个抽象语法树,第二个参数是sourceCode,第三个参数是一些转换的Options
// 这里解析后的code 就是可以在浏览器运行的代码
const { code } = babel.transformFromAst(ast, null, {
// 插件的集合-将es6语法转换为es5
presets: ["@babel/preset-env"]
});
return {
filename,
dependencies,
code
}
}
// 依赖图谱,存储所有模块的依赖信息,entry是入口文件,我们要分析整个项目所有的文件;
const makeDependenciesGraph = (entry) => {
const entryModule = moduleAnalyser(entry);
// 利用队列的方法,循环递归获取模块中的依赖文件进行分析
const graphArry = [entryModule];
for(let i = 0; i < graphArry.length; i++) {
const item = graphArry[i];
const { dependencies } = item;
if (dependencies) {
// for in 循环对象
for(let j in dependencies) {
graphArry.push(
moduleAnalyser(dependencies[j])
);
}
}
}
// 将数组进行转换为对象
const graph = {};
graphArry.forEach(item => {
graph[item.filename] = {
dependencies: item.dependencies,
code: item.code
}
})
return graph;
}
const graphInfo = makeDependenciesGraph('./src/index.js');
console.log(graphInfo);
上面的代码是获取到了整个项目中代码的依赖以及依赖的分析结果,接下来,我们要实现的是将这些分析结果变成真正能够在浏览器上运行的代码:
// nodeJS中的模块,用于获取文件信息
const fs = require('fs');
// 引入babel/parser用来分析我们的源代码
const parser = require('@babel/parser');
// 引入babel/traverse来帮助我们分析抽象语法树中 type = 'ImportDeclaration' 的节点,default是我们使用export default导出的内容
const traverse = require('@babel/traverse').default;
// 引入nodeJS的核心模块 path
const path = require('path');
// 引入babel/core来准换我们的代码
const babel = require('@babel/core');
//filename为要分析的入口文件
const moduleAnalyser = (filename) => {
// 读取文件中的内容,第一个参数为文件地址,第二个参数为文件编码方式
const content = fs.readFileSync(filename, 'utf-8')
const ast = parser.parse(content, {
// 如果要对于es6语法的代码进行分析,需要传入一个sourceType选项,sourceType: 'module'
sourceType: 'module'
});
// 存放依赖的文件- 相对路径与绝对路径
const dependencies = {}
// 第一个参数为抽象语法树类型的参数,第二个参数为需要查找的内容,
// 这里我们查找ImportDeclaration节点的内容,只要包含他,就会走这个函数,
traverse(ast, {
// 获取到该节点的node节点
ImportDeclaration({ node }) {
// 获取到filename的路径 也就是主入口文件的路径 ./src
const dirname = path.dirname(filename);
// 将相对路径转换为绝对路径 ./src/message.js
const newFile = './'+ path.join(dirname, node.source.value).replace('\\', '/');
// 存储相对路径与绝对路径
dependencies[node.source.value] = newFile;
// dependencies.push(newFile);
}
});
// 借助babel的transformFromAst方法将抽象语法树转换为可以运行的代码。
// 第一个参数是一个抽象语法树,第二个参数是sourceCode,第三个参数是一些转换的Options
// 这里解析后的code 就是可以在浏览器运行的代码
const { code } = babel.transformFromAst(ast, null, {
// 插件的集合-将es6语法转换为es5
presets: ["@babel/preset-env"]
});
return {
filename,
dependencies,
code
}
}
// 依赖图谱,存储所有模块的依赖信息,entry是入口文件,我们要分析整个项目所有的文件;
const makeDependenciesGraph = (entry) => {
const entryModule = moduleAnalyser(entry);
// 利用队列的方法,循环递归获取模块中的依赖文件进行分析
const graphArry = [entryModule];
for(let i = 0; i < graphArry.length; i++) {
const item = graphArry[i];
const { dependencies } = item;
if (dependencies) {
// for in 循环对象
for(let j in dependencies) {
graphArry.push(
moduleAnalyser(dependencies[j])
);
}
}
}
// 将数组进行转换为对象
const graph = {};
graphArry.forEach(item => {
graph[item.filename] = {
dependencies: item.dependencies,
code: item.code
}
})
return graph;
}
const generateCode = (entry) => {
//const graph = makeDependenciesGraph(entry);
const graph = JSON.stringify(makeDependenciesGraph(entry));
// 这里使用闭包的形式,是为了防止执我们的代码污染到全局。
return `
(function(graph){
//构造require以及exports函数
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 code = generateCode('./src/index.js');
console.log(code)
4. 通过 CreateReactApp 深入学习 Webpack 配置
使用命令npx create-react-app my-app
创建一个react
项目,我们可以运行命令npm run eject
暴露项目配置,就可以看到有关webpack
的配置信息,有可能你会出现下面的错误:
Remove untracked files, stash or commit any changes, and try again.
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! my-app@0.1.0 eject: `react-scripts eject`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the my-app@0.1.0 eject script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
npm ERR! A complete log of this run can be found in:
npm ERR! C:\Users\Dell\AppData\Roaming\npm-cache\_logs\2019-06-22T06_24_24_879Z-debug.log
这个是git
配置的问题,是因为我们使用脚手架创建一个项目的时候,自动给我们增加了一个.gitignore
文件,而我们本地却没有文件仓库,我们只需要将我们的项目添加到我们本地的仓库,输入下面命令:
git add .
git commit -m "create app"
npm run eject
就可以了。我们可以看到项目中会多出现几个文件夹,查看Script
中的build.js
文件,里面就是打包流程的逻辑代码,主要的配置文件是在config
文件夹中的webpack.config.js
中。path.js
中主要是存储整个项目的一些路径信息,env.js
初始化项目运行环境的文件。webpackDevServer.config.js
文件。具体查看配置源码,进行深入。
5. Vue cli 3.0
vue
的脚手架工具,并没有像react
的一样可以通过命令暴露项目配置,他也是有一套默认的配置,如果想要修改默认配置,需要添加一个vue.config.js
的配置文件,然后安装官网给出的配置参数进行配置,那些配置参数都是vue-cli
通过封装了的参数。查看一些配置参数:https://cli.vuejs.org/zh/config/#css-loaderoptions,我们可能会想,vue
是如何将自己的配置转换成了webpack
的配置文件,可以在node_module
中找到@vue
中的vli-service
中lib
的service.js
文件,这个文件就是打包的时候进行转换的。
对于webpack
配置的学习可以查看官网,一般基础的配置可以查看guides
中的内容,如果要查看深入的配置可以看看configuration
里面的内容,如果想要写一些loader
或者是plugin
可以查看api
相关的内容。
在vue-cli
中,访问项目中的静态资源文件,必须要通过require()
函数进行加载,我们可以修改webpack
配置,增加如下配置:这样static
目录中的文件在外部也可以进行访问了,告诉服务器从哪里提供内容。只有在您想要提供静态文件时才需要这样做。
const path = require('path');
module.exports = {
devServer: {
contentBase: [path.resolve(__dirname, 'static')],
}
}