Webpack学习笔记
webpack - 项目优化
webpack实现原理
webpack - loader
webpack - plugin
webpack - 项目优化2
创建自己的webpack 全局命令mywebpack
创建一个名为 mywebpack 的项目,目录结构如下:
|- bin
|- main.js
|- package.json
// package.json
"bin": {
"mywebpack": "./bin/main.js"
},
// main.js
#! /usr/bin/env node
console.log('hahaha')
在目录下执行sudo npm link
创建全局npm命令,指向开发目录执行
/usr/local/bin/mywebpack -> /usr/local/lib/node_modules/mywebpack/bin/main.js
/usr/local/lib/node_modules/mywebpack -> /Users/xxxx/xxxx/08.02-mywebpack
实现一个简单的webpack
- main.js
#! /usr/bin/env node
// 读取配置文件
const path = require('path')
const Compile = require('../src/Compile')
let config_path = path.resolve('webpack.config.js')
let config = require(config_path)
let com = new Compile(config)
com.run()
- src/Compile.js
const fs = require('fs')
const path = require('path')
const babel = require('@babel/core')
const t = require('@babel/types')
// 所有用到的文件,以文件路径为Key,创建对像
// value为内容,且require被替换为__webpack_require__
// 引入文件路径改为基于根目录路径
// 确定入口文件,执行内容
//
// 创建依赖图谱,把所有依赖做成列表
// 把模板 和 我们解析出来的列表 进行渲染 打包到目标文件中
class Compile {
constructor (config) {
this.config = config
this.entry = config.entry
this.root = process.cwd() // 目标项目物理路径
this.import_tree = {} // 引用结构 {路径: 代码}
// console.log(config)
}
run () {
this.handlerCode(this.entry, true)
// console.log('runed', this.import_tree)
this.packagingTemplate()
}
// 拼装模板
packagingTemplate () {
let ejs = require('ejs')
let template = fs.readFileSync(path.join(__dirname, './template.ejs'))
template = ejs.render(template, { entry: this.entry, modules: this.import_tree });
let output_dir = path.join(this.config.output.path, this.config.output.filename)
fs.writeFileSync(output_dir, template)
}
handlerCode (filepath, is_main) {
// 读取文件
let in_code = fs.readFileSync(path.join(this.root, filepath))
let relative_path = path.relative(this.root, filepath)
let parent_path = path.dirname(relative_path)
let ast = this.handlerAst(in_code, parent_path)
// console.log('run helf', ast.imports)
ast.imports.forEach(dir => {
if (!this.import_tree[dir]) this.handlerCode(dir)
})
// console.log('run', JSON.stringify(this.import_tree))
this.import_tree[filepath] = ast.out_code
}
handlerAst (in_code, parent_path) {
// 转ast树
let imports = []
let out = babel.transform(in_code, {
plugins: [{ visitor: { CallExpression ({node}) {
// 找到require代码
if (node.callee.name == 'require') {
// require => __webpack_require__
node.callee.name = '__webpack_require__'
let import_file = './' + path.join(parent_path, node.arguments[0].value)
// 引用文件路径改为基于根目标
node.arguments[0].value = import_file
// 递归引用的文件
imports.push(import_file)
}
} } }]
})
return {out_code: out.code, imports: imports}
}
}
module.exports = Compile
- src/template.ejs
...
// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = "<%= entry %>");
})
/************************************************************************/
({
<% for (let key in modules) { %>
"<%- key %>": (function(module, exports, __webpack_require__) {
eval("<%- modules[key].replace(/\n/g, '\\n').replace(/"/g, '\\"') %>\n\n//# sourceURL=webpack:///<%- key %>?");
}),
<% } %>
});
loaders
- 目标项目 webpack.config.js 中添加
module: {
rules: [
{
test: /\.txt$/,
use: [ path.resolve(__dirname, 'loaders', 'txt.js') ]
}
]
}
- 目标项目 loaders/txt.js
必须是个方法,入参为.txt文件内容
module.exports = function (source) {
console.log(source)
return source
}
webpack
执行后打包成功,并打印出了文件内容。下面开始实现
- src/Compile.js 在资源加载(读取文件处,单独拿出一个方法)时处理loaders
...
readSource (filepath) {
let source = fs.readFileSync(path.join(this.root, filepath), 'utf8') // 从下向上执行
this.config.module.rules.reduceRight((perv, item) => {
let {test: reg, use} = item
if (reg.test(filepath)) {
let loader = require(use[0])
source = loader(source)
}
}, '')
return source
}
...
实现less-loader
- 目标项目 webpack.config.js 中添加
...
{
test: /\.less$/,
// use: [ 'style-loader', 'css-loader', 'less-loader' ] 从左向右执行
use: [
path.resolve(__dirname, 'loaders', 'style.js'),
path.resolve(__dirname, 'loaders', 'less.js'),
]
}
- 目标项目 loaders/less.js
let less = require('less')
module.exports = function (source) {
let css = ''
less.render(source, (err, r) => { // render是同步的 [黑人问号脸]
css = r.css
})
return css
}
- 目标项目 loaders/style.js
module.exports = function (source) {
// JSON.stringify可以处理引号问题
let code = `
let style = document.createElement('style')
style.innerHTML = ${JSON.stringify(source).replace(/\\n/g,'\\\n')}
document.head.append(style)
`
return code
}
- src/Compile.js 在资源加载(读取文件处,单独拿出一个方法)时处理loaders
...
readSource (filepath) {
let source = fs.readFileSync(path.join(this.root, filepath), 'utf8')
this.config.module.rules.reduceRight((perv, item) => { // 从下向上执行
let {test: reg, use} = item
if (reg.test(filepath)) {
use.reduceRight((perv, loader_name) => { // 从左向右执行
let loader = require(loader_name)
source = loader(source)
}, '')
}
}, '')
return source
}
...
如果loader是异步的
- 目标项目 loaders/less.js 不返回数据,使用异步回调
...
let cb = this.async() // webpack给的异步回调方法
setTimeout(() => cb(null, css), 2000) // 方法要2个入参,err, source
}
- src/Compile.js 改动就大发了
async run () {
console.log('runing')
await this.handlerCode(this.entry, true)
console.log('runed', this.import_tree)
this.packagingTemplate()
}
readSource (filepath) {
let source = fs.readFileSync(path.join(this.root, filepath), 'utf8')
return new Promise((resolve) => {
let rules = this.config.module.rules
let rule_index = rules.length - 1
let loader_list = []
let loader_index = -99
let isSync = false
let next = () => {
if (rule_index < 0) return resolve(source);
if (loader_index < 0) {
// 读取下一项配置
let {test: reg, use} = rules[rule_index--]
if (reg.test(filepath)) {
loader_list = use
loader_index = use.length - 1
} else return next();
}
isSync = false
let loader = require(loader_list[loader_index--])
let new_source = loader.call(this, source)
if (!isSync) setvalueFn(null, new_source)
}
// 赋值并next
let setvalueFn = (err, new_source) => {
source = new_source
next()
};
// 异步标识及回调
this.async = () => {
isSync = true
return setvalueFn
}
next()
})
// return source
}
async handlerCode (filepath, is_main) {
// 读取文件
console.log('----', filepath)
let in_code = await this.readSource(filepath)
let ast = this.handlerAst(in_code, filepath)
for (let i = 0; i < ast.imports.length; i++) {
if (!this.import_tree[ast.imports[i]]) await this.handlerCode(ast.imports[i])
}
this.import_tree[filepath] = ast.out_code
}
plugins
webpack先解析plugins,再解析loader(pulgin会在run之前执行)
- 目标项目 webpack.config.js 中添加
plugins: [
//
new A, new B
]
}
class A {
apply () { // 必须提供apply方法
console.log('A')
}
}
class B {
apply (compiler) { // compiler上有hooks,为webpack内部的钩子们 => tapable的SyncHook实例
console.log('B')
}
}
在任意loader头部添加console,执行后会A B loader ...
打印compiler下的hooks可以看到它上面的钩子们
hooks上的部分钩子- tapable的用法
// 非node下的event,发布订阅
let { SyncHook } = require('tapable')
let s = new SyncHook(['a', 'b'])
s.tap('11111', function (a,b,c) {
console.log('11', a,b,c)
})
s.tap('22222', function (a,b,c) {
console.log('22', a,b,c)
})
s.call('i', 'am', 'girl')
- src/Compile.js
let { SyncHook } = require('tapable')
...
constructor (config) {
...
// 使用tapable实现钩子
this.hooks = {
beforeRun: new SyncHook(['arg']),
afterEmit: new SyncHook(['arg'])
}
// 处理plugin
this.config.plugins.forEach(pg => {
pg.apply(this)
})
}
async run () {
this.hooks.beforeRun.call('something')
...
handlerAst (in_code, filepath) {
...
this.hooks.afterEmit.call('something')
return {out_code: out.code, imports: imports}
}
class B {
apply (compiler) {
console.log('B')
compiler.hooks.beforeRun.tap('test', function () {
console.log('beforeRun')
})
compiler.hooks.afterEmit.tap('test', function () {
console.log('afterEmit')
})
}
}
执行后结果A B beforeRun afterEmit loader afterEmit
网友评论