1.webpack 模块化实现
项目里面引入了moment
import moment from 'moment'
在非debug状态下,尝试输入moment是拿不到moment这个模块的,但是debug代码的时候不能直接拿到moment这个模块,而是通过__WEBPACK_IMPORTED_MODULE_18_moment___default可以拿到
按理说moment()调用一次就拿到了moment对象,然而并不可以
__WEBPACK_IMPORTED_MODULE_18_moment___default()()
调用了两次才拿到
onTimeChange = val => {
const mon = moment //这边通过
console.info(val)
}
从上图可知 debug 状态还是无法再控制台输出moment(),而赋值给变量mon就可以执行mon()拿到moment对象,而__WEBPACK_IMPORTED_MODULE_18_moment___default()()
还是要执行两次才能拿到moment对象
webpack务虚扫盲
从 Bundle 文件看 Webpack 模块机制
webpack模块加载原理
2. require.context
官网的解释:
require.context(directory:String, includeSubdirs:Boolean /* optional, default true */, filter:RegExp /* optional */)
Specify a whole group of dependencies using a path to the directory
, an option to includeSubdirs
, and a filter
for more fine grained control of the modules included. These can then be easily resolved later on:
var context = require.context('components', true, /\.html$/);//获取components目录下所有以.html结尾的文件(包括其子目录)
var componentA = context.resolve('componentA');
使用webpack的require.context实现路由“去中心化”管理
webpack require context 说明
3. 使用ejs作为HtmlWebpackPlugin的模板
通常情况下我们使用HTML作为单页面的模板,
new HtmlWebpackPlugin({ // Also generate a test.html
filename: 'test.html',
template: 'src/assets/test.html'
})
但是有时候我们需要在页面用到变量或者遍历数组,那么就需要使用模板引擎,比如ejs,
<script src="<%= htmlWebpackPlugin.options.devServer %>/webpack-dev-server.js" type="text/javascript"></script>
上面的htmlWebpackPlugin.options.devServer变量 来自于webpack.config.js
new HtmlWebpackPlugin({
inject: false,
template: '../index.ejs',
appMountId: 'app',
devServer: 'http://localhost:3001', // 看这里
mobile: true,
lang: 'en-US',
title: 'My App',
window: {
env: {
apiHost: 'http://myapi.com/api/v1'
}
}
})
]
具体配置参考jaketrent/html-webpack-template
小白学react之EJS模版实战
4. vscode debug webpack
我们在启动项目的时候都是使用npm script启动的,那么怎么用vscode启动项目并debug webpack的打包呢?
首先配置vscode launch.json
{
"type": "node",
"request": "launch",
"name": "Launch via NPM with dll",
"runtimeExecutable": "npm",
"runtimeArgs": ["run-script", "dll"],
"port": 9229
},
一开始做的尝试node --inspect-brk ./node_modules/.bin/webpack --config config/webpack.config.dll.js
但是报错
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") ^^^^^^^ SyntaxError: missing ) after argument list
看了这篇issue node --inspect-brk ./node_modules/.bin/webpack --config config/webpack.config.dll.js 报错解决办法 修改为node --inspect-brk ./node_modules/webpack/bin/webpack --config config/webpack.config.dll.js
这样就可以调试了,哈哈哈
报错的原因好像是操作系统的问题,.bin目录下是shell和cmd文件,直接node --inspect-brk ./node_modules/.bin/webpack
就执行shell,而在Windows上执行node --inspect-brk ./node_modules/.bin/webpack.cmd
也会报错
SyntaxError: missing ) after argument list
我的项目是用create-react-app创建的,并且做了eject,然后webpack的配置全部暴露出来了,启动项目"start": "node scripts/start.js",
尝试上面的方法配置webpack,debug会报错,然后重新做了配置"start:debug": "node --nolazy --inspect-brk=9229 scripts/start.js",
,vscode launch.json加上如下代码,
{
"type": "node",
"request": "launch",
"name": "Launch via NPM with start",
"runtimeExecutable": "npm",
"runtimeArgs": ["run-script", "start:debug"],
"port": 9229
},
这样不但start.js webpack.config.dev.js断点能进入,webpack的loader也可以断点进入,比如css-loader,但是去掉所有的断点,然后启动,要过很久才能加载出页面。
5. style-resources-loader@1.2.1导致cra创建的typescript项目编译很卡
我用cra新建的项目的webpack的版本是4.39.1,style-resources-loader@1.2.1依赖的webpack的版本是^4.16.5,改变一行代码,编译下来要20s,升级style-resources-loader到1.3.3之后,编译只要5秒的样子,提升了几倍。style-resources-loader@1.3.3 依赖webpack@4.41.3,我项目里安装的是webpack@4.39.1,不晓得都是webpack@4.x为啥编译速度差别这么大.
刨根问底,在style-resources-loaderissue找到了答案,在我的mac上编译是不卡的,但是在windows上很卡,Failed to cache .less files on Win 10 with Webpack 4 #17是因为webpack升级到4有些代码改了,而该库没有做出对不同系统文件系统路径兼容的处理,导致代码编译的样式没有被缓存,是的每次rebuild都会去重新编译样式文件。
6. 关于webpack是怎么将css 跟js是怎么插入到dom中的
cra 新建的项目dev环境默认有三个文件,webpack@4.42.0
由图可以看出来,script标签插入HTML字符串是在afterTemplateExecution
跟beforeEmit
之间完成的,做了代码测试看到的结果也是如此,扒开源码看看
文件名 | 作用 | 解释 |
---|---|---|
bundle.js | webpack runtime代码 | 包括webpack-dev-server |
0.chunk.js | 第三方库代码 | react react-dom等 |
main.chunk.js | 自己写的代码 | 包括自己写的js跟css |
webpack 怎样把样式代码插入到js中的
webpack怎样实现对样式文件的缓存的
是style-loader还是html-webpack-plugin将style标签插入到dom中的
style-loader@1.0.0源码各种拼接字符串还是很难读的,找到一篇不错的源码解析文章webpack loader 从上手到理解系列 style-loader
{loaderUtils.stringifyRequest(
this,
`!!${request}`
)}
!!
从未见过,在js里是将其他类型转为boolean,官方文档显示,有三种使用 loader 的方式
内联(inline)
可以在 import
语句或任何 等同于 "import" 的方法 中指定 loader。使用 !
将资源中的 loader 分开。每个部分都会相对于当前目录解析。
import Styles from 'style-loader!css-loader?modules!./styles.css';
使用 !
或者!
或者-!
为整个规则添加前缀,可以覆盖webpack.config.js配置中的所有 loader 定义。具体规则如下:
- Prefixing with ! will disable all configured normal loaders
import Styles from '!style-loader!css-loader?modules!./styles.css';
- Prefixing with !! will disable all configured loaders (preLoaders, loaders, postLoaders)
import Styles from '!!style-loader!css-loader?modules!./styles.css';
- Prefixing with -! will disable all configured preLoaders and loaders but not postLoaders
import Styles from '-!style-loader!css-loader?modules!./styles.css';
这边提到了preLoaders ,postLoaders ,在webpack 1.0是有这个配置的,v2就拿掉了,但是概念还是在的,变成了enforce配置,如下:
module: {
- preLoaders: [
+ rules: [
{
test: /\.js$/,
+ enforce: "pre",
loader: "eslint-loader"
}
]
}
pre将该loader设置到最前面,post将该loader设置到最后面。但是loader的执行是有两个阶段的。
pitching 阶段:loader 上的 pitch 方法,按照
后置(post)、行内(normal)、普通(inline)、前置(pre)
的顺序调用。更多详细信息,请查看 越过 loader(pitching loader)。normal阶段:loader 上的 常规方法,按照
前置(pre)、行内(normal)、普通(inline)、后置(post)
的顺序调用。模块源码的转换,发生在这个阶段。
官方文档举了两个阶段的例子
module.exports = {
//...
module: {
rules: [
{
//...
use: [
'a-loader',
'b-loader',
'c-loader'
]
}
]
}
};
将会发生这些步骤:
|- a-loader `pitch`
|- b-loader `pitch`
|- c-loader `pitch`
|- requested module is picked up as a dependency
|- c-loader normal execution
|- b-loader normal execution
|- a-loader normal execution
这也就解释了style-loader为什么代码都写在module.exports.pitch
中而且module.exports
返回的是空函数。
webpack怎样将js插入到dom里的
html-webpack-plugin的index.js -> postProcessHtml函数 -> injectAssetsIntoHtml函数 -> 赋值给webpack的compilation.assets
整个流程HTML都是字符串,然后HTML字符串交给了webpack的Compiler.js处理html字符串是怎样写入到文件里的?
content = Buffer.from(bufferOrString, "utf8")//将字符串转为Buffer
this.outputFileSystem.writeFile(targetPath, content, err => {})//outputFileSystem为fs或者memory-fs,content为Buffer
就这两个重要步骤
html-webpacl-plugin apply方法,官方注释已经写得很明确
const injectedHtmlPromise = Promise.all([assetTagGroupsPromise, templateExectutionPromise])
// Allow plugins to change the html before assets are injected
.then(([assetTags, html]) => {
const pluginArgs = { html, headTags: assetTags.headTags, bodyTags: assetTags.bodyTags, plugin: self, outputName: childCompilationOutputName };
return getHtmlWebpackPluginHooks(compilation).afterTemplateExecution.promise(pluginArgs);
})
.then(({ html, headTags, bodyTags }) => {
return self.postProcessHtml(html, assets, { headTags, bodyTags });//将<srcipt src="xxxx">标签插入HTML字符串
});
const emitHtmlPromise = injectedHtmlPromise
// Allow plugins to change the html after assets are injected
.then((html) => {
const pluginArgs = { html, plugin: self, outputName: childCompilationOutputName };
return getHtmlWebpackPluginHooks(compilation).beforeEmit.promise(pluginArgs)
.then(result => result.html);
})
.catch(err => {
// In case anything went wrong the promise is resolved
// with the error message and an error is logged
compilation.errors.push(prettyError(err, compiler.context).toString());
// Prevent caching
self.hash = null;
return self.options.showErrors ? prettyError(err, compiler.context).toHtml() : 'ERROR';
})
.then(html => {
// Allow to use [templatehash] as placeholder for the html-webpack-plugin name
// See also https://survivejs.com/webpack/optimizing/adding-hashes-to-filenames/
// From https://github.com/webpack-contrib/extract-text-webpack-plugin/blob/8de6558e33487e7606e7cd7cb2adc2cccafef272/src/index.js#L212-L214
const finalOutputName = childCompilationOutputName.replace(/\[(?:(\w+):)?templatehash(?::([a-z]+\d*))?(?::(\d+))?\]/ig, (_, hashType, digestType, maxLength) => {
return loaderUtils.getHashDigest(Buffer.from(html, 'utf8'), hashType, digestType, parseInt(maxLength, 10));
});
// Add the evaluated html code to the webpack assets
compilation.assets[finalOutputName] = {
source: () => html,//此处的HTML任然为字符串
size: () => html.length
};
return finalOutputName;
})
.then((finalOutputName) => getHtmlWebpackPluginHooks(compilation).afterEmit.promise({
outputName: finalOutputName,
plugin: self
}).catch(err => {
console.error(err);
return null;
}).then(() => null));
再次验证script标签是在afterTemplateExecution跟beforeEmit插入到HTML字符串中的,但是没有写入到文件中,只是将拼接好的HTML字符串,交给了webpack
compilation.assets
是个对象,存了文件名跟对应文件内容的字符串。
HtmlWebpackPlugin index.js
emitHtmlPromise.then(() => {
callback();//出发了callAsync的回调
});
Compiler.js emitAssets方法
这里调用了memory-fs的mkdirp创建目录,然后根据文件分别新建对应的文件
// Write the file to output file system
this.outputFileSystem.writeFile(targetPath, content, err => {})
而memory-fswriteFileSync就是将文件用二进制的形式存在了current对象里
字符串转为二进制对象
content = Buffer.from(bufferOrString, "utf8");
至于怎样将这个对象显示在chrome的source面板中的,有空再探索。
webpacl Compilerthis.outputFileSystem
的实例属性默认为null,start.js新建devServer const devServer = new WebpackDevServer(compiler, serverConfig);
传递webpack compiler实例到dev server ,然后dev server再将compiler传递到dev middleware ,webpack-dev-middleware@3.7.2 fs.js setFs函数中给outputFileSystem
赋值
const MemoryFileSystem = require('memory-fs');
fileSystem = new MemoryFileSystem();
// eslint-disable-next-line no-param-reassign
compiler.outputFileSystem = fileSystem;
同样我们如果在webpack-dev-server配置writeToDisk:true,fs.js文件的toDisk函数调用fs.writeFile将文件写入磁盘
const fs = require('fs');
return fs.writeFile(targetPath, content, (writeFileError) => {
return callback();
});
在构建的时候没有dev server是怎样将文件写入磁盘的?
const webpack = (options, callback) => {
let compiler;
if (typeof options === "object") {
compiler = new Compiler(options.context);
compiler.options = options;
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);//调用plugin的apply函数
}
}
}
}
return compiler;
};
NodeEnvironmentPlugin.js
class NodeEnvironmentPlugin {
constructor(options) {
this.options = options || {};
}
apply(compiler) {
...
compiler.inputFileSystem = new CachedInputFileSystem(
new NodeJsInputFileSystem(),
60000
);
const inputFileSystem = compiler.inputFileSystem;
compiler.outputFileSystem = new NodeOutputFileSystem();//给outputFileSystem赋值
compiler.watchFileSystem = new NodeWatchFileSystem(
compiler.inputFileSystem
);
...
}
}
module.exports = NodeEnvironmentPlugin;
NodeOutputFileSystem.js
const fs = require("fs");
const path = require("path");
const mkdirp = require("mkdirp");
class NodeOutputFileSystem {
constructor() {
this.mkdirp = mkdirp;
this.mkdir = fs.mkdir.bind(fs);
this.rmdir = fs.rmdir.bind(fs);
this.unlink = fs.unlink.bind(fs);
this.writeFile = fs.writeFile.bind(fs);//其实就是调用node原生的fs
this.join = path.join.bind(path);
}
}
module.exports = NodeOutputFileSystem;
最后在Compiler.js line 434,调用node的 fs.writeFile函数写入文件到磁盘
// Write the file to output file system
this.outputFileSystem.writeFile(targetPath, content, err => {})
webpack是怎样监测文件的变化的?
loader 在什么时候执行的?
loader-runner负责执行loader
你真的掌握了loader么?- loader十问
从零实现一个 Webpack Loader
plugin跟loader是并行执行的吗?有什么先后顺序吗?