Webpack底层原理及脚手架工具分析

1. 如何编写一个Loader
1.1 实现一个简单的Loader

我们开始写一个打包之后的文件,将js代码中jie这个字符串替换为world的一个loader,首先我们新建一个功能,使用npm init,然后进行安装webpacknpm 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:这里配置loaderuse属性也是一个对象,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'
                }
            }
        ]
        }]
    },

如果这样,我们可以使用一个reaolveLoaderresolveLoader代码意思是,如果我们引用一个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

首先要知道loaderplugin之间的关系: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-servicelibservice.js文件,这个文件就是打包的时候进行转换的。

对于webpack配置的学习可以查看官网,一般基础的配置可以查看guides中的内容,如果要查看深入的配置可以看看configuration里面的内容,如果想要写一些loader或者是plugin可以查看api相关的内容。

vue-cli中,访问项目中的静态资源文件,必须要通过require()函数进行加载,我们可以修改webpack配置,增加如下配置:这样static目录中的文件在外部也可以进行访问了,告诉服务器从哪里提供内容。只有在您想要提供静态文件时才需要这样做。

const path = require('path');

module.exports = {
	devServer: {
		contentBase: [path.resolve(__dirname, 'static')],
	}
}
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jiegiser#

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值