const bundle = file => { const depsGraph = JSON.stringify(parseModules(file)) return `(function(graph){ function require(file) { var exports = {}; return exports } require('${file}') })(${depsGraph})` }
接下来内容其实很简单,就是我们取得入口文件的code信息,去执行它就好了,使用eval函数执行,初步写出代码如下:
const bundle = file => { const depsGraph = JSON.stringify(parseModules(file)) return `(function(graph){ function require(file) { var exports = {}; (function(code){ eval(code) })(graph[file].code) return exports } require('${file}') })(${depsGraph})` }
上面的写法是有问题的,我们需要对file做绝对路径转化,否则 graph[file].code 是获取不到的,定义adsRequire方法做相对路径转化为绝对路径
const bundle = file => { const depsGraph = JSON.stringify(parseModules(file)) return `(function(graph){ function require(file) { var exports = {}; function absRequire(relPath){ return require(graph[file].deps[relPath]) } (function(require, exports, code){ eval(code) })(absRequire, exports, graph[file].code) return exports } require('${file}') })(${depsGraph})` }
接下来,我们只需要执行bundle方法,然后把生成的内容写入一个JavaScript文件即可
const content = bundle('https://www.jb51.net/article/src/index.js') // 写入到dist/bundle.js fs.mkdirSync('./dist') fs.writeFileSync('./dist/bundle.js', content)
最后,我们在index.html引入这个 ./dist/bundle.js 文件,我们可以看到控制台正确输出了我们想要的结果
4. bundle.js的完整代码
const fs = require('fs') const path = require('path') const parser = require('@babel/parser') const traverse = require('@babel/traverse').default const babel = require('@babel/core') const getModuleInfo = file => { const body = fs.readFileSync(file, 'utf-8') console.log(body) const ast = parser.parse(body, { sourceType: 'module' }) // console.log(ast.program.body) const deps = {} traverse(ast, { ImportDeclaration({ node }) { const dirname = path.dirname(file); const absPath = './' + path.join(dirname, node.source.value) deps[node.source.value] = absPath } }) const { code } = babel.transformFromAst(ast, null, { presets: ["@babel/preset-env"] }) const moduleInfo = { file, deps, code } return moduleInfo } const parseModules = file => { // 定义依赖图 const depsGraph = {} // 首先获取入口的信息 const entry = getModuleInfo(file) const temp = [entry] for (let i = 0; i < temp.length; i++) { const item = temp[i] const deps = item.deps if (deps) { // 遍历模块的依赖,递归获取模块信息 for (const key in deps) { if (deps.hasOwnProperty(key)) { temp.push(getModuleInfo(deps[key])) } } } } temp.forEach(moduleInfo => { depsGraph[moduleInfo.file] = { deps: moduleInfo.deps, code: moduleInfo.code } }) // console.log(depsGraph) return depsGraph } // 生成最终可以在浏览器运行的代码 const bundle = file => { const depsGraph = JSON.stringify(parseModules(file)) return `(function(graph){ function require(file) { var exports = {}; function absRequire(relPath){ return require(graph[file].deps[relPath]) } (function(require, exports, code){ eval(code) })(absRequire, exports, graph[file].code) return exports } require('${file}') })(${depsGraph})` } const build = file => { const content = bundle(file) // 写入到dist/bundle.js fs.mkdirSync('./dist') fs.writeFileSync('./dist/bundle.js', content) } build('https://www.jb51.net/article/src/index.js')
2. 手写 loader 和 plugin
2.1 如何自己实现一个 loader
loader本质上就是一个函数,这个函数会在我们在我们加载一些文件时执行
2.1.1 如何实现一个同步 loader
首先我们初始化一个项目,项目结构如图所示:
其中index.js和webpack.config.js的文件内容如下:
// index.js console.log('我要学好前端,因为学好前端可以: ') // webpack.config.js const path = require('path') module.exports = { mode: 'development', entry: { main: 'https://www.jb51.net/article/src/index.js' }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].js' } }
我们在根目录下创建 syncLoader.js ,用来实现一个同步的loader,注意这个函数必须返回一个 buffer 或者 string
// syncloader.ja module.exports = function (source) { console.log('source>>>>', source) return source }