vlambda博客
学习文章列表

搭建一个webpack多目录部署项目

一、首先在开始写webpack之前要完成各种环境变量的准备

ROOT_PATH = (exports.ROOT_PATH = path.resolve(__dirname,'./'));// 找到项目的根目录STATIC_PATH = (exports.STATIC_PATH = path.join(ROOT_PATH, 'static'));// 找到静态资源的路径PAGE_PATH = (exports.PAGE_PATH = path.join(STATIC_PATH, 'page'));// 找到部署页面的目录

二、获取部署目录下所有页面路径的方法

function envPaths() {// PROJECT_PATH project_path DIR dir 是输入命令行的变量
// 例如PROJECT_PATH = 项目目录1,项目目录2 npm start 获取到项目目录名称// reduce的一个参数是callback函数 第二个参数是[] 作为函数第一个参数的默认值// callback函数的第一个参数是累计器累计回调的返回值; 它是上一次调用回调时返回的累积值// callback函数的第二个参数是数组中正在处理的元素 // 第三个参数是数组中正在处理的当前元素的索引// 第四个参数是调用reduce()的数组 var paths = ['PROJECT_PATH', 'project_path', 'DIR', 'dir'].reduce(function( dirs, key) { // process.env[key] key 是 输入的变量 比如 project_path if (!process.env[key]) {     // 如果process.env[key]是undefined 就返回[] 表示没有部署的目录 return dirs; } return dirs.concat(    // process.env[key] 输入多目录时获取的目录名称是 react,vue,node process.env[key].split(',').filter(function(name) {      // fs.existsSync判断文件是否真的存在      // 如果输入的目录真实存在就返回true // 目录或者是入口js存在的情况 return ( fs.existsSync(path.join(PAGE_PATH, name)) ||          // 入口ts存在的情况 用ts代替/js$/ fs.existsSync(path.join(PAGE_PATH, name.replace(/js$/, 'ts'))) || // 入口tsx存在的情况 fs.existsSync(path.join(PAGE_PATH, name.replace(/js$/, 'tsx'))) ); }) ); }, []);
if (!paths.length) { paths.push('fake'); console.info('You may should set PROJECT_PATH or DIR environment variable'); }// paths就是最后多目录部署的目录名称 return paths;}

三、获取部署目录下的所有符合条件的部署文件

function pathEntries(dirs) { // dirs 多目录部署的目录名称 return dirs.reduce(function(entries, dir) {  // 部署目录的绝对路径    var dirPath = path.join(PAGE_PATH, dir); // 如果传入的是一个入口文件, 则直接放入数组中返回    // 匹配-entry并且jsx或者tsx结尾的文件    // fs.statSync(dirPath)返回给定文件的路径信息 size birthtime等    // fs.statSync(dirPath).isFile() 返回一个文件是否是文件 if (dirPath.match(/-entry\.[tj]sx?$/)) { if (fs.existsSync(dirPath) && fs.statSync(dirPath).isFile()) { return entries.concat(dirPath); } var tsEntry = dirPath.replace(/js$/, 'ts'); if (fs.existsSync(tsEntry) && fs.statSync(tsEntry).isFile()) { return entries.concat(tsEntry); } var tsxEntry = dirPath.replace(/js$/, 'tsx'); if (fs.existsSync(tsxEntry) && fs.statSync(tsxEntry).isFile()) { return entries.concat(tsxEntry);      } return entries; }
// 如果不是入口文件, 则读取目录下的所有入口文件 // 不允许嵌套目录, 只取单层目录下的入口文件, 子目录下的不管    // fs.readdirSync 读取目录下的所有文件  return entries.concat( fs .readdirSync(dirPath)        .filter(function(item{ return ( item.match(/-entry\.[tj]sx?$/) && fs.statSync(path.join(dirPath, item)).isFile() ); }) .map(function(item) { // 返回全路径 return path.join(dirPath, item); }) ); }, []);}

最终代码的实现

项目目录

var path = require('path');var fs = require('fs');var ROOT_PATH = (exports.ROOT_PATH = path.resolve(__dirname, '.'));
module.exports =pathEntries(envPaths()).map(function(entryFile){ var entryPath = path.dirname(entryFile); var entryDir = path.relative(ROOT_PATH, entryPath); return { entry:function() { var result = {}; result[entryDir] = entryFile; return result; }, output: { path: path.resolve('./') + '/dist', filename:'[name]_bundle.js', libraryTarget: 'umd', umdNamedDefine: true }, mode: "development", resolve: { extensions: ['.js', '.jsx'] }, devServer: { contentBase: path.join(__dirname, 'dist'), port: 8787, inline: false }, module: { rules: [{ test: /\.(css|scss)$/, use: ['style-loader', 'css-loader', 'sass-loader'] }, { test: /\.(js|jsx|es6)$/, loader: 'babel-loader?cacheDirectory', exclude: /node_modules/, options: { presets: ['@babel/preset-env', '@babel/preset-react'], plugins: ['@babel/plugin-proposal-class-properties'] } }] }}})
function pathEntries(dirs) { return dirs.reduce(function(entries, dir) { var dirPath = path.join(ROOT_PATH, dir); return entries.concat( fs .readdirSync(dirPath) .filter(function(item) { return ( item.match(/index\.jsx?$/) && fs.statSync(path.join(dirPath, item)).isFile() ); }) .map(function(item) { // 返回全路径 return path.join(dirPath, item); }) ); }, []);}
function envPaths() { var paths = ['PROJECT_PATH', 'project_path', 'DIR', 'dir'].reduce(function( dirs, key) { if (!process.env[key]) { return dirs; } return dirs.concat( process.env[key].split(',').filter(function(name) { // 目录或者是入口js存在的情况 return ( fs.existsSync(path.join(ROOT_PATH, name)) ); }) ); }, []);
if (!paths.length) { paths.push('fake'); console.info('You may should set PROJECT_PATH or DIR environment variable'); }
return paths;}


科普阶段

一、学习一下以上用到的path方法

1.path.resolve()path.resolve:方法会把一个路径或路径片段的序列解析为一个绝对路径例如:
const path1 = path.resolve('/a/b', '/c/d'); // 输出: /c/dconst path2 = path.resolve('/a/b', 'c/d'); // 输出:/a/b/c/dconst path3 = path.resolve('/a/b', '../c/d'); // 输出:/a/c/dconst path4 = path.resolve('a', 'b');// 输出:/Users/xiao/work/test/a/b
resolve把‘/’当成根目录 path.resolve()方法可以将多个路径解析为一个规范化的绝对路径。其处理方式类似于对这些路径逐一进行cd操作,与cd操作不同的是,这引起路径可以是文件。并且可不必实际存在(resolve()方法不会利用底层的文件系统判断路径是否存在,而只是进行路径字符串操作);
如下所示path.resolve('www', 'static', '../public', 'src', '..');// cd www /Users/xiao/work/test/www// cd static /Users/xiao/work/test/www/static// cd ../public /Users/xiao/work/test/www/public// cd src /Users/xiao/work/test/www/public/src// cd .. /Users/xiao/work/test/www/public
2.与path.resolve()操作分不开的是path.join()操作二者区别如下1.join是把各个path片段连接在一起, resolve把‘/’当成根目录path.join('/a', '/b'); // /a/bpath.resolve('/a', '/b');// /b2.resolve在传入非/路径时,会自动加上当前目录形成一个绝对路径,而join仅仅用于路径拼接// 当前路径为/Users/xiao/work/testpath.join('a', 'b', '..', 'd');// a/dpath.resolve('a', 'b', '..', 'd');// /Users/xiao/work/test/a/d
3.__dirname __filename process.cwd() ./ 或者 ../ 的区别__dirname 获得当前执行文件所在目录的完整目录名__filename: 总是返回被执行的 js 的绝对路径process.cwd(): 总是返回运行 node 命令时所在的文件夹的绝对路径./: 跟 process.cwd() 一样,返回 node 命令时所在的文件夹的绝对路径
注意点 项目目录如下所示react-page -index/ -nodejs/ -1.findLargest.js -2.path.js -3.fs.js -regs -regx.js -test.txt在项目根目录react-page下运行node index/nodejs/3.fs.js3.fs 内容如下fs.readFile('./1.findLargest.js',(err)=>{  console.log(err) // no such file or directory })// 原因是当前运行脚本的目录是react-page目录 但是文件的目录是/Users/jawil/Desktop/nodejs/demo/ES6-lottery/syntax/nodejs所以找不到文件
在刚才报错的 3.fs 文件中require('./1.findLargest.js');却可以输出{A:1} 因为require是编译时执行,read是运行时执行
4.path.relativepath.relative() 方法根据当前工作目录返回 fromto 的相对路径path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb');// 返回: '../../impl/bbb'
5.path.basenamepath.basename('/目录1/目录2/文件.html');// 返回: '文件.html'path.basename('/目录1/目录2/文件.html', '.html');// 返回: '文件'
6.path.extnamepath.extname('index.html');// 返回: '.html'path.extname('index.coffee.md');// 返回: '.md'

二、process.env

process.env 项目运行所在环境的一些信息如何配置环境变量process.env.NODE_ENV == 'dev' ? 'http://dev.com' : process.env.NODE_ENV == 'product' ? 'http://product.com':'localhost:3000'比如运行项目时输入的一些参数也在process.env下例如 PROJECT_PATH = 项目目录1,项目目录2 npm startprocess.env.PROJECT_PATH 就可以获取到运行的 项目目录1 和 项目目录2

三、webpack devTool的属性值

1.eval 每个 module 会封装到 eval 里包裹起来执行,并且会在末尾追加注释 //@ sourceURLwebpackJsonp([1],[ function(module,exports,__webpack_require__){ eval( ... //# sourceURL=webpack:///./src/js/index.js?' )  },...])2.source-map生成一个 SourceMap 文件webpackJsonp([1],[ function(e,t,i){...}, function(e,t,i){...}, function(e,t,i){...}, function(e,t,i){...}, ...])// # sourceMappingURL=index.js.map// 与此同时,你会发现你的 output 目录下多了一个 index.js.map 文件。// 看看这个index.js.map{ "version":3, "sources":[ "webpack:///js/index.js", "webpack:///./src/js/index.js", "webpack:///./~/.npminstall/css-loader/0.23.1/css-loader/lib/css-base.js", ... ], "names":["webpackJsonp","module","exports"...], "mappings":"AAAAA,cAAc,IAER,SAASC...", "file":"js/index.js", "sourcesContent":[...], "sourceRoot":""}3.hidden-source-map和 source-map 一样,但不会在 bundle 末尾追加注释 与 source-map 相比少了末尾的注释,但 output 目录下的 index.js.map 没有少4.inline-source-map生成一个 DataUrl 形式的 SourceMap 文件 webpackJsonp([1],[ function(e,t,i){...}, function(e,t,i){...}, function(e,t,i){...}, function(e,t,i){...}, ... ]) //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9...5.eval-source-map每个 module 会通过 eval() 来执行,并且生成一个 DataUrl 形式的 SourceMap 6.cheap-source-map生成一个没有列信息(column-mappings)的 SourceMaps 文件,不包含 loader 的 sourcemap(譬如 babel 的 sourcemap)和 source-map 生成结果差不多。output 目录下的 index.js 内容一样。但是cheap-source-map生成的 index.js.map 的内容却比 source-map 生成的 index.js.map 要少很多代码,我们对比一下上文 source-map 生成的index.js.map 的结果,发现 source 属性里面少了列信息,只剩一个"webpack:///js/index.js"// index.js.map{ "version":3, "file":"js/index.js", "sources":["webpack:///js/index.js"], "sourcesContent":[...], "mappings":"AAAA", "sourceRoot":""}7.cheap-module-source-map生成一个没有列信息(column-mappings)的 SourceMaps 文件,同时 loader 的 sourcemap 也被简化为只包含对应行的。开发环境推荐:cheap-module-eval-source-map生产环境推荐:cheap-module-source-map (这也是下版本 webpack 使用-d命令启动 debug 模式时的默认选项)原因如下:**使用 cheap 模式可以大幅提高 souremap 生成的效率。**大部分情况我们调试并不关心列信息,而且就算 sourcemap 没有列,有些浏览器引擎(例如 v8) 也会给出列信息。**使用 eval 方式可大幅提高持续构建效率。**参考官方文档提供的速度对比表格可以看到 eval 模式的编译速度很快。使用 module 可支持 babel 这种预编译工具(在 webpack 里做为 loader 使用)。**使用 eval-source-map 模式可以减少网络请求。**这种模式开启 DataUrl 本身包含完整 sourcemap 信息,并不需要像 sourceURL 那样,浏览器需要发送一个完整请求去获取 sourcemap 文件,这会略微提高点效率。而生产环境中则不宜用 eval,这样会让文件变得极大。

四、学习MiniCssExtractPlugin如何做到的分开打包

在 Webpack 4 之前,我们使用 extract-text-webpack-plugin 插件来提取项目中引入的样式文件,打包到一个单独的文件中从 Webpack 4 开始,这个插件就过时了,需要使用 MiniCssExtractPlugin此插件为每个包含 CSS 的 JS 文件创建一个单独的 CSS 文件,并支持 CSS 和 SourceMap 的按需加载。注意:这里说的每个包含 CSS 的 JS 文件,并不是说组件对应的 JS 文件,而是打包之后的 JS 文件!接下来会详细说明。
const MiniCssExtractPlugin = require('mini-css-extract-plugin');module.exports = { plugins: [ new MiniCssExtractPlugin({ filename: '[name].css' }), ], module: { rules: [ { test: /\.css$/, use: [ MiniCssExtractPlugin.loader, 'css-loader','postcss-loader' // postcss-loader 可选 ], },{ test: /\.less$/, use: [ MiniCssExtractPlugin.loader, 'css-loader','postcss-loader','less-loader' // postcss-loader 可选 ], } ], },};

情景一

// 入口文件 app.jsimport Root from './components/Root'
// Root.jsimport '../styles/main.less'import Topics from './Topics'
// Topics.jsimport "../styles/topics.less"
这种情况下,Topics 会和 Root 同属一个 chunk,所以会一起都打包到 app.js 中, 结果就是 main.less 和 topics.less 会被提取到一个文件中:app.css。而不是生成两个 css 文件。

情景二

但是,如果 Root.js 中并没有直接引入 Topics 组件,而是配置了代码分割 ,比如模块的动态引入,那么结果就不一样因为这个时候有两个 chunk,对应了两个 JS 文件,所以会提取这两个 JS 文件中的 CSS 生成对应的文件。这才是“为每个包含 CSSJS 文件创建一个单独的 CSS 文件”的真正含义。

情景三

但是,如果分割了 chunk,还是只希望只生成一个 CSS 文件怎么办呢?也是可以做到的。但需要借助 Webpack 的配置 optimization.splitChunks.cacheGroupsoptimization.splitChunks 是干什么的呢?在 Webpack 4 以前,我们使用 CommonsChunkPlugin 来提取重复引入的第三方依赖,比如把 ReactJquery 单独提取到一个文件中。而从 Webpack 4 开始,CommonsChunkPluginoptimization.splitChunks 替代了。从命名也能看出来,它是用来拆分 chunk 的。怎么在这里需要用到这个配置呢?先来看看配置怎么写的:optimization: { splitChunks: { cacheGroups: { // Extracting all CSS/less in a single file styles: { name: 'styles', test: /\.(c|le)ss$/, chunks: 'all', enforce: true, }, } }}打包结果: Asset Size Chunks Chunk Names app.js 281 KiB 2 [emitted] [big] app styles.bundle.js 402 bytes 0 [emitted] styles styles.css 332 bytes 0 [emitted] styles topics.bundle.js 2.38 KiB 5 [emitted] topics

可以看出,样式确实都被提取到一个 styles.css 文件中了。但与此同时多了一个 style.bundle.js 文件,这就是 optimization.splitChunks.cacheGroups 的效果。具体原理就不在此深究,感兴趣的话可以研究一下。

MiniCssExtractPlugin vs style-loader 区别

MiniCssExtractPlugin 提取 JS 中引入的 CSS 打包到单独文件中,然后通过标签 <link>添加到头部;style-loader 则是通过 <style> 标签直接将 CSS 插入到 DOM 中。通常,基本的 CSS 配置都是类似这样的。先 style-loader,然后 css-loader。module: { rules: [ { test: /\.css$/, use: [ 'style-loader', 'css-loader' ], }, ],}但后来由于想要提取 CSS 到单独的文件里,就需要用上 MiniCssExtractPlugin。那么问题来了,如下的配置可行吗?{ test: /\.css$/, use: [ 'style-loader', MiniCssExtractPlugin.loader, 'css-loader','postcss-loader' ],}生产模式根据 MiniCssExtractPlugin 文档 中说到的,此插件适用于没有style-loader 的生产模式中,以及需要 HMR 的开发模式。This plugin should be used only on production builds without style-loader in the loaders chain, especially if you want to have HMR in development.也就是说,在生产模式中,以上的配置同时使用了style-loader 和 MiniCssExtractPlugin 是不合适的(试了一下,style-loader不会起作用)。我们只能取其一。也可以如下两者结合,开发模式中使用 style-loader,生产模式中使用 MiniCssExtractPlugin。各取所需,毕竟这两者的作用还是很不同。{ test: /\.css$/, use: [ devMode?'style-loader':MiniCssExtractPlugin.loader,'css-loader','postcss-loader' ]}样式文件热更新(HMR)在开发模式中, 我们可以用 MiniCssExtractPlugin 实现样式的 HMR(Hot Module Replacement,模块热更新)样式文件的 HMR 是指什么呢?如果没有配置 HMR,开发模式下,修改 CSS 源文件的时候,页面并不会自动刷新加载修改后的样式。需要手动刷新页面,才会加载变化。而 HMR 实现了被修改模块的热更新,使得变化即时显示在页面上,不再需要刷新整个页面。 style-loader 实现了 HMR 接口因此开发环境下,这两个插件都是可以热更新 CSS 的,只是 MiniCssExtractPlugin 的配置可能更丰富一些。比如说:style-loader 只热更新 JS 中引入的样式,如果 index.html 中通过 <link> 引入了服务器中的一个CSS 文件如果开发模式下,修改 test.css 的源码,style-loader 不会热更新变化 CSS,而是需要刷新整个页面,但 MiniCssExtractPlugin 则会自动重新加载所有的样式。const devMode = process.env.NODE_ENV === 'development'; // 是否是开发模式//......module.exports = { //...... module: { rules:[ { test: /\.less$/i, use: [ { loader: MiniCssExtractPlugin.loader, options: { // 只在开发模式中启用热更新 hmr: devMode, // 如果模块热更新不起作用,重新加载全部样式 reloadAll: true, }, }, 'css-loader','postcss-loader','less-loader' ] }, // ...... ] }}

五、webpack的output

output: { filename: 打包后的文件名称      path: 打包后的目录      chunkFilename:指未被列在 entry 中,却又需要被打包出来的 chunk 文件的名称。一般来说,这个 chunk 文件指的就是要懒加载的代码。      publicPath: 在复杂的项目里可能会有一些构建出的资源需要异步加载,加载这些异步资源需要对应的 URL 地址。 output.publicPath 配置发布到线上资源的 URL 前缀,为string 类型。默认值是空字符串 '',即使用相对路径。                   示例 : filename:'[name]_[chunkhash:8].js'                  publicPath: 'https://cdn.example.com/assets/'                  <script src='https://cdn.example.com/assets/a_12345678.js'></script>          crossOriginLoading:Webpack 输出的部分代码块可能需要异步加载,而异步加载是通过 JSONP 方式实现的。JSONP 的原理是动态地向 HTML 中插入一个 <script src="url"></script> 标签去加载异步资源。output.crossOriginLoading 则是用于配置这个异步插入的标签的 crossorigin 值。                         script 标签的 crossorigin 属性可以取以下值:                         anonymous(默认) 在加载此脚本资源时不会带上用户的 Cookies; use-credentials 在加载此脚本资源时会带上用户的 Cookies。 通常用设置 crossorigin 来获取异步加载的脚本执行时的详细错误信息。      libraryTarget 和 library :当用 Webpack 去构建一个可以被其他模块导入使用的库时需要用到它们。 output.libraryTarget 配置以何种方式导出库。                                output.library 配置导出库的名称。
                                output.libraryTarget 是字符串的枚举类型,支持以下配置                                var(默认):// Webpack 输出的代码                                                                          var LibraryName = lib_code; // 使用库的方法 LibraryName.doSomething();                                commonJS:// Webpack 输出的代码                                         exports['LibraryName'] = lib_code; // 使用库的方法 require('library-name-in-npm')['LibraryName'].doSomething();                                commonjs2:// Webpack 输出的代码                                          module.exports = lib_code; // 使用库的方法 require('library-name-in-npm').doSomething();                                   this: // Webpack 输出的代码                                            this['LibraryName'] = lib_code;                                             // 使用库的方法 this.LibraryName.doSomething();                                     window:// Webpack 输出的代码                                            window['LibraryName'] = lib_code;                                             // 使用库的方法 window.LibraryName.doSomething();                                     global:// Webpack 输出的代码                                            global['LibraryName'] = lib_code;                                                                                        // 使用库的方法                                            global.LibraryName.doSomething();                                                                                                              }                                     libraryExport                                     output.libraryExport 配置要导出的模块中哪些子模块需要被导出。 它只有在 output.libraryTarget 被设置成 commonjs 或者 commonjs2 时使用才有意义。                                     假如要导出的模块源代码是:                                     export const a=1;                                     export default b=2;                                     现在你想让构建输出的代码只导出其中的 a,可以把 output.libraryExport 设置成 a,那么构建输出的代码和使用方法将变成如下:                                     // Webpack 输出的代码                                     module.exports = lib_code['a'];                                     // 使用库的方法                                     require('library-name-in-npm')===1;                                                         一、chunkFilename比如说我们业务代码中写了一份懒加载 lodash 的代码async function getAsyncComponent() { const { default: _ } = await import('lodash');    element.innerHTML = _.join(['Hello!', 'dynamic', 'imports', 'async'], ' '); return element;}output.chunkFilename 默认使用 [id].js 或从 output.filename 中推断出的值([name] 会被预先替换为 [id][id].)如果我们显式配置 chunkFilename,就会按配置的名字生成文件

搭建一个webpack多目录部署项目

六、webpack的hash、chunkhash、contenthash

对于webpack的hash,常用于cdn缓存[hash] is a "unique hash generated for every build" //每次build都重新生成[chunkhash] is "based on each chunks' content" //根据每个块的内容生成[contenthash] is "generated for extracted content" // 提取的内容生成[hash:8] 代表取8位 Hash 值,默认是20位
hashmodule.exports = { // mode: 'development', // mode: 'production', entry: { index: './src/index.js', detail: './src/detail.js', }, output: { filename: '[name].[hash].js', path: path.resolve(__dirname, 'dist') },}上面代码运行生成的index文件和detail文件的hash是一样的只改index.js文件它的hash串变了,detail.js的hash串也会变
chunkhash 每一个文件最后的hash根据它引入的chunk决定// file1.jsconsole.log('file1')// file2.jsconsole.log('file2')// file3.jsconsole.log('file3')// index.jsrequire('./file2')console.log('index')// detail.jsrequire('./file1')console.log('detail')变更// index.jsrequire('./file2')require('./file3')console.log('index')index的hash变了但是detail的hash也变了原因是 module identifier,因为 index 新引入的模块改变了以后所有模块的 id 值,所以 detail 文件中引入的模块 id 值发生了改变,于是 detail 的 chunkhash 也随着发生改变。webpack已经提供方案了,解决方案是将默认的数字 id 命名规则换成路径的方式。webpack 4 中当 mode 为 development 会默认启动,但是production环境还是默认的id方式,webpack也提供了相应的plugin来解决这个问题plugins: [ new webpack.HashedModuleIdsPlugin(),]加上这个plugin后,变更后,detail的hash串仍然没有变化,符合预期。在webpack中,有css的情况下,每个entry file会打包出来一个js文件和css文件,在使用chunkhash的情况下,js和css的文件的hash会是一样的,这个时候暴露出来的一个问题:你修一个react的bug,但是并没有改样式,最后更新后,js和css的文件的hash都变了。这个还是不太好,css文件的hash串不变最好,再继续升级!contenthashcontenthash是根据抽取到的内容来生成hash生产环境使用一个MiniCssExtractPlugin来进行css的压缩,在plugin里面指定hash为contenthash,修改js文件后,js文件的hash串变了,css的hash串没变!完美。new MiniCssExtractPlugin({ filename: '[name].[contenthash:8].css', chunkFilename: '[name].[contenthash:8].chunk.css'})optimization.moduleIds: "hashed"也能达到相同的效果所以目前最佳实践是contenthash+HashedModuleIdsPlugin/optimization.moduleIds: "hashed"

七、externals用法详解

externals来防止第三方依赖包被打包
UMD模块规范输出的模块所以兼容commonjs模块规范(在node环境中使用)。兼容AMD模块规范(用require.js引入使用)。兼容CMD模块规范(用sea.js引入使用)。

八、mode

mode可设置development production两个参数如果没有设置,webpack4 会将 mode 的默认值设置为 productionproduction模式下会进行tree shaking(去除无用代码)和uglifyjs(代码压缩混淆)

九、优化打包速度

影响前端发布速度的有两个方面,一个是构建,一个就是压缩,把这两个东西优化起来,可以减少很多发布的时间。

1.mode 的默认值设置为 production 进行tree shaking和uglifyjs(代码压缩混淆)2.缩小文件的搜索范围当我们代码中出现 import 'vue'时, webpack会采用向上递归搜索的方式去node_modules 目录下找。  为了减少搜索范围我们可以直接告诉webpack去哪个路径下查找。也就是别名(alias)的配置。3.noParse 当我们代码中使用到import jq from 'jquery'时,webpack会去解析jq这个库是否有依赖其他的包。 但是我们对类似jquery这类依赖库,一般会认为不会引用其他的包(特殊除外,自行判断)。增加noParse属性,告诉webpack不必解析,以此增加打包速度。4.(第一张图)extensions webpack会根据extensions定义的后缀查找文件(频率较高的文件类型优先写在前面)5.(第二张图)使用HappyPack开启多进程Loader转换6.(第三张图)使用webpack-parallel-uglify-plugin 增强代码压缩

更多webpack优化请看这篇文章,本文优化打包速度参考这篇文章

https://zhuanlan.zhihu.com/p/99959392

thread-loader 优化构建

示例var threadLoader = require('thread-loader');var cssWorkerPool = { // 一个 worker 进程中并行执行工作的数量 // 默认为 20 workerParallelJobs: 2, poolTimeout: 2000};threadLoader.warmup(cssWorkerPool, ['css-loader', 'postcss-loader']);var jsWorkerPool = { // options // 产生的 worker 的数量,默认是 (cpu 核心数 - 1) // 当 require('os').cpus() 是 undefined 时,则为 1 workers: 2, // 闲置时定时删除 worker 进程 // 默认为 500ms // 可以设置为无穷大, 这样在监视模式(--watch)下可以保持 worker 持续存在 poolTimeout: 2000 };threadLoader.warmup(jsWorkerPool, ['babel-loader']);module:{  rules:[ {      test/\.s?css$/, oneOf: [ { loader: [ isIncludes ? MiniCssExtractPlugin.loader : { loader: 'style-loader' }, { loader: 'thread-loader', options: cssWorkerPool }, { loader: 'css-loader', options: { minimize: true }}, { loader: 'sass-loader' } ] } ] },     ]}每个 worker 都是一个单独的有 600ms 限制的 node.js 进程。同时跨进程的数据交换也会被限制。请在高开销的loader中使用,否则效果不佳

十、代码分割的三种方式

详细参考文章 快狗打车前端

https://juejin.cn/post/6844904103848443912


入口起点:使用 entry 配置手动地分离代码。动态导入:通过模块的内联函数调用来分离代码。防止重复:使用 splitChunks 去重和分离 chunk。 第一种方式,很简单,只需要在 entry 里配置多个入口即可entry: { app: "./index.js", app1: "./index1.js" }第二种方式,就是在代码中自动将使用 import() 加载的模块分离成独立的包:import("./a");第三种方式,splitChunks 默认配置splitChunks: { // 表示选择哪些 chunks 进行分割,可选值有:async,initial和all chunks: "async", // 表示新分离出的chunk必须大于等于minSize,默认为30000,约30kb。 minSize: 30000, // 表示一个模块至少应被minChunks个chunk所包含才能分割。默认为1 minChunks: 1, // 表示按需加载文件时,并行请求的最大数目。默认为5 maxAsyncRequests: 5, // 表示加载入口文件时,并行请求的最大数目。默认为3 maxInitialRequests: 3, // 表示拆分出的chunk的名称连接符。默认为~。如chunk~vendors.js automaticNameDelimiter: '~', // 设置chunk的文件名。默认为true。当为true时,splitChunks基于chunk和cacheGroups的key自动命名。 name: true, // cacheGroups 下可以可以配置多个组,每个组根据test设置条件,符合test条件的模块,就分配到该组。模块可以被多个组引用,但最终会根据priority来决定打包到哪个组中。默认将所有来自 node_modules目录的模块打包至vendors组,将两个以上的chunk所共享的模块打包至default组。 cacheGroups: { vendors: { test: /[\\/]node_modules[\\/]/, priority: -10 }, //  default: { minChunks: 2, priority: -20, reuseExistingChunk: true } }}以上配置,概括如下4个条件:模块在代码中被复用或者来自 node_modules 文件夹模块的体积大于等于30kb(压缩之前)当按需加载 chunks 时,并行请求的最大数量不能超过5初始页面加载时,并行请求的最大数量不能超过将3index.js 作为入口文件,属于入口起点手动配置分割代码的情况,因此会独立打包。(app.js)a.js 通过 import() 进行加载,属于动态导入的情况,因此会独立打出一个包。(1.js)vue 来自 node_modules 目录,并且大于30kb;将其从 a.js 拆出后,与 a.js 并行加载,并行加载的请求数为2,未超过默认的5;vue 拆分后,并行加载的入口文件并无增加,未超过默认的3。vue 也符合 splitChunks 的拆分条件,单独打了一个包(2.jschunks 用以告诉 splitChunks 的作用对象,其可选值有 asyncinitialall默认值是 async,也就是默认只选取异步加载的chunk进行代码拆分。chunks 值为 initial 时,splitChunks 的作用范围变成了非异步加载的初始 chunk例如我们的 index.js 就是初始化的时候就存在的chunk。而 vue 模块是在异步加载的chunk a.js 中引入的,所以并不会被分离出来。

十、babel 我学废了

babel 原理

https://www.zoo.team/article/babel-2


一、@babel-core 的作用是把js代码分析成ast,方便各个插件分析语法进行相应的处理。    有些新语法在低版本js 中是不存在的,如箭头函数,rest 参数,函数默认值等,   这种语言层面的不兼容只能通过将代码转为ast,分析其语法后再转为低版本js。二、@babel/preset-env 是一个灵活的预设,你可以无需管理目标环境需要的语法转换或浏览器 polyfill ,就可以使用最新的JavaScript。三、@babel/plugin-transform-runtime 该插件会开启对Babel 注入的 helper code ( helper 可译为辅助函数)的复用,以节省代码体积。   这些用到的辅助函数都从 @babel/runtime 中去加载,这样就可以做到代码复用了四、@babel/preset-react react语法包,这个包,是专门作为react的优化,让你在代码中可以使用React ES6 classes的写法,同时直接支持JSX语法格式。   @babel/preset-react是@babel/plugin-syntax-jsx、@babel/plugin-transform-react-jsx和@babel/plugin-transform-react-display-name五、@babel/plugin-proposal-decorators 可以使用装饰器

十、想要运行webpack-dev-server之后自动打开页面的两种方式

第一种"scripts": {   "start""cross-env NODE_ENV=development webpack-dev-server --config webpack.config.dev.js --open",}第二种devServer: { contentBase: path.join(__dirname, 'dist'), port: 9999, inline: false,    open:true}