简易版webpack实现
项目搭建
-
打开cmd,创建一个webpack-dev目录
mkdir webpack-dev cd webpack-dev
把项目拖进vscode中,初始化项目,执行
yarn init -y
-
构建项目目录结构
|-- webpack-dev |-- package-lock.json |-- package.json |-- yarn-error.log |-- yarn.lock |-- bin | |-- xs-pack.js |-- lib | |-- Compiler.js | |-- main.ejs |-- src
-
修改package.json,添加bin字段
//package.json { "name": "webpack-dev", "version": "1.0.0", "main": "index.js", "license": "MIT", "bin":{ "xs-pack":"./bin/xs-pack.js" } }
-
指定文件编译环境,在xs-pack.js顶部加上
#! /usr/bin/env node
-
把包文件link到全局下,并添加xs-pack命令,在项目下打开cmd输入
npm link
代码编写
-
编写xs-pack.js文件
作用:接收webpack.config.js文件,把这个文件传入Compiler类中,进行解析
#! /usr/bin/env node //1.需要找到当前执行命令的路径,webpack.config.js let path = require('path') //config配置文件 let config = require(path.resolve(__dirname)) //解析类 let Compiler = require('../lib/Compiler.js') //把拿到的webpack.config.js传入解析类中 let compiler = new Compiler(config) // 标识运行编译 compiler.run()
-
编写Compiler.js
作用:
获取入口文件路径,保存模块依赖。
递归依赖模块,获取相应的源码内容
把模块源码收集,最终打包出去
-
函数初始化
//接收外部传进来的webpack.config.js文件 //进行数据初始化 constructor(config){ //外部webpack配置文件 this.config = config // 保存入口文件的路径 './src/index.js' this.entryId // 保存所有的模块依赖 this.modules = {} this.entry = config.entry //入口路径 webpack.config.js中的entry字段 this.root = process.cwd() //当前工作路径 ,xs-pack命令 执行的路径,一般为项目根路径 }
-
bundleModule方法:接收入口模块路径,获取模块中的内容,同时递归广度遍历依赖模块中的内容。
bundleModule(modulePath, isEntry) { // 拿到模块文件中的源码内容 let source = this.getSource(modulePath) let moduleName = './' + path.relative(this.root, modulePath) //相对路径 if (isEntry) {//判断是否是入口模块 this.entryId = moduleName //保存入口名字 } //解析把source源码进行改造 返回一个依赖列表 let { sourceCode, dependencies } = this.parse( source, path.dirname(moduleName) ) // path.dirname(moduleName) ./src //把相对路径和模块中的内容,进行关联起来 this.modules[moduleName] = sourceCode dependencies.forEach(dep => { //依赖模块加载 this.bundleModule(path.join(this.root, dep), false) }) } //获取文件源码函数,封装出来利于复用 getSource(modulePath) { let content = fs.readFileSync(modulePath, 'utf8') return content }
-
parse源码解析改造方法
作用:解析模块源码,改造源码中的路径
这是webpack打包后bundle.js文件的部分源码内容,从图中我们可以看出该文件的引入路径跟我们书写的项目文件路径是不同的,我们需要把这些路径改造。
所以当前的首要问题是如何改造源码?可以通过AST抽象语法树。
关于AST抽象语法树不懂的同学可以阅读这篇文章https://segmentfault.com/a/1190000016231512
我们借助几个工具来完成这个源码改造babylon :把源码转换为ast
@babel/traverse :遍历节点
@babel/types :主要用途是在创建AST的过程中判断各种语法的类型。
@babel/generator: 将AST解码生 js代码。
安装它们
yarn add -D babylon @babel/traverse @babel/types @babel/generator
想学习有关babel知识的同学可以参考https://www.jianshu.com/p/9aaa99762a52
parse方法具体实现
parse(source, parentPath) { //把源码转化为ast语法树 let ast = babylon.parse(source) //依赖模块数组 let dependencies = [] //编译ast,修改源码 traverse(ast, { CallExpression(p) { let node = p.node if (node.callee.name === 'require') { //webpack自己实现了一个require方法:__webpack_require__ //所以我们要把项目中所有require变成 __webpack_require__ node.callee.name = '__webpack_require__' //模块引入路径 require('./a') => ./a let moduleName = node.arguments[0].value //添加文件后缀 ./a =》 ./a.js moduleName = moduleName + (path.extname(moduleName) ? '' : '.js') //添加文件路径 ./a.js =》 ./src/a.js moduleName = './' + path.join(parentPath, moduleName) //在依赖列表中保存当前路径 dependencies.push(moduleName) //把修改好的路径重新放进ast中 node.arguments = [t.stringLiteral(moduleName)] } } }) //AST 解析语法树 //把ast转化为源码 let sourceCode = generator(ast).code return { sourceCode, dependencies } }
-
emitFile渲染模板文件方法
作用:把获取到的全部模块内容编写进模板文件中,也就是webpack打包后bundle.js文件
emitFile() { // 用数据 渲染我们的模板 //获取webpack.config.js 出口文件字段信息 //拼接路径 let main = path.join( this.config.output.path,//出口文件路径 this.config.output.filename//出口文件名 ) //获取ejs模板,该找里面的内容 let templateStr = this.getSource(path.join(__dirname, 'main.ejs')) //把获取的模块内容渲染进模块 let code = ejs.render(templateStr, { entryId: this.entryId, modules: this.modules }) //考虑到后面可能需要打包多入口文件,使用对象存放。 this.assets = {} this.assets[main] = code //把生成的模板内容,放进出口文件中并生成出来 fs.writeFileSync(main, this.assets[main]) }
我们这里使用的是ejs模板,需要进行安装
yarn add ejs
编写ejs模板
main.ejs
这段模板其实就是webpack生成的bundle.js中的最主要的功能实现的那部分。
我们只需修改下入口文件和参数依赖项即可
main.ejs文件;(function(modules) { var installedModules = {} function __webpack_require__(moduleId) { if (installedModules[moduleId]) { return installedModules[moduleId].exports } var module = (installedModules[moduleId] = { i: moduleId, l: false, exports: {} }) modules[moduleId].call( module.exports, module, module.exports, __webpack_require__ ) module.l = true return module.exports } return __webpack_require__((__webpack_require__.s = '<%-entryId%>')) })({ <%for(let key in modules){%> "<%-key%>":(function(module, exports, __webpack_require__) { eval( `<%-modules[key]%>` ) }), <%}%> })
-
使用xs-pack
创建test项目
=
编写测试文件
编写webpack.config.js文件
let path =require('path') module.exports = { mode:'development', entry: './src/index.js', output: { filename: 'bundle.js', path:path.resolve(__dirname,'dist') } }
由于我们先前已经把xs-pack导入到全局环境中,我们只需要在当前项目根路径下打开命令行输入
xs-pack
最后即可在dist文件下看到打包成功后的文件,可以创建一个html引入这个js,并在浏览器上查看。
-
项目已上传至github
总结
以上就是一个简易版webpack的实现,从里面我们可以看出webpack打包的原理,就是一些文件路径的处理,文件源码的改造。这个简易版webpack,仅仅是简单的打包下文件,像webpack的module,plugin,resolve等功能并未实现。