一、多页面开发
如果公司的产品需要做很多活动,一大堆促销活动,邀请活动,周年活动,每个活动之间没有关联,每个活动是独立的。 那么你们就需要做成多页面应用。
那么多页面还用 react,用 webpack 吗? 当然的,毕竟模块化,组件化,不管是开发还是维护上都是舒服太多了。(用 vue 也可以,看公司技术栈)
核心思路:
1.用 nodejs 遍历 src 目录下的文件,找出多页面的入口 Js 文件,以及对应的 HTML 模板(生成的文件,最终要注入到 html 模板才有意义, 因为不同的活动,模板可能不同,因此做成每个活动一个 html 模板)
2.匹配入口 JS 文件对应的 html 模板,然后用 HtmlWebpackPlugin插件 生成 html,并且注入对应的 js 文件
3.把生成的 html plugins 列表,放到 webpack 的插件配置中
实现目标:
npm run build //打包所有文件
npm run build demo1 //打包单个项目demo1
npm run build demo1,demo2 //同时打包多个项目
npm run server //开启本地服务
step 1:在webpack.config.base.js文件中找出多页面的入口文件js
const fs = require('fs');
const optimist = require("optimist");
const cateName = optimist.argv.cate;
const entryPath = __dirname + '/src' + '/category/';
let entryObj = {};
if(cateName ===true){
/*直接输入npm run build打包所有文件*/
fs.readdirSync(entryPath).forEach((cateName)=>{
if(cateName !== "index.html"&&cateName!==".DS_Store"){
entryObj[cateName+'/'+cateName]=entryPath + cateName + "/" + cateName + '.js';
}
});
}else if(cateName.indexOf(",")){
/*一次性打包多个入口文件以逗号分割 如:npm run build demo1,demo2*/
var cateNameArray = cateName.split(',');
for(var i=0;i<cateNameArray.length;i++){
entryObj[cateNameArray[i] + '/' + cateNameArray[i]] = entryPath + cateNameArray[i] + '/' + cateNameArray[i] + '.js';
}
}else {
/*打包单个入口文件*/
entryObj[cateName+"/"+cateName] = entryPath + cateName + '/' + cateName + '.js';
}
let config = {
mode: 'none',
entry: entryObj, //多入口文件以对象的形式设置每个入口js
output: {
libraryTarget: 'umd',
path:__dirname +'/dist/',
filename: "[name].js" //出口js文件
},
devServer: {
contentBase: "./src",//本地服务器所加载的页面所在的目录
port:"8080",//设置监听端口
historyApiFallback: true,//不跳转
inline: true//源文件变更时,实时刷新
},
module:{}
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css' //css文件单独打包
})
]
}
module.exports = {
config:config,
entryObj:entryObj
};
step2:在webpack.config.js文件中匹配入口 JS 文件对应的 html 模板,并通过HtmlWebpackPlugin插件 生成 html且引入相应资源。最终插入到webpack配置中
var {config,entryObj} = require('./webpack.config.base');
var pages = Object.keys(entryObj);
var HTMLWebpackPlugin = require('html-webpack-plugin');
pages.forEach(function (pathname) {
var template_local = entryObj[pathname].replace('.js',".html");
var entryName = pathname.split("/")[0];
var conf = {
filename: entryName+'/' + entryName + '.html', //生成的html存放路径
title:entryName,
template: template_local, //html模板路径
inject: true, //js插入的位置,true/'head'/'body'/false
hash: true, //为静态资源生成hash值
chunks: [pathname],//需要引入的chunk,不配置就会引入所有页面的资源
minify: { //压缩HTML文件
removeComments: true, //移除HTML中的注释
collapseWhitespace: false //删除空白符与换行符
}
};
config.plugins.push(new HTMLWebpackPlugin(conf));
});
module.exports= config;
最终多页面打包效果如图
二、UglifyJsPlugin插件
如果你有良好编程实践,你可能注重代码的可读性,因此你在代码中添加了大量的空白符(制表符、空格、换行符)和注释。在代码变得更漂亮的同时,也使得文件的体积大大增加了。另一方面,为了用户体验(指减少文件体积)而牺牲可读性也不可取,手工这么做的话很繁琐。因此,这儿有一种解决方案供你在项目中选择。
Webpack4中引入了一个新参数:mode。要求总是在配置中指定。如果不指定,会产生一个警告并退而其次的使用默认值,默认值就是production。如果你使用 mode:"production", Webpack将通过UglifyJSPlugin插件对代码进行压缩。而mode:“development”时的开发环境,为了减少打包耗时,往往不对代码进行压缩。
在package.json文件中可以对开发、生产环境的打包命令进行设置
"scripts": {
"dev": "webpack --mode development --cate",
"build": "webpack --mode production --cate"
}
特殊的,在开发环境也可以手动配置UglifyJSPlugin插件,进行压缩
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
{
mode: 'development',
optimization: {
minimize: true,
minimizer: [
new UglifyJsPlugin()
]
},
entry:entryObj,
output:{},
devServer:{},
module:{},
plugins:{}
}
三、tree shaking
1.是什么?
我们常常碰到这样的案例,需要从某文件中命名导出(某一个或几个变量、函数、对象等),然而这个文件还有许多其它(我们这次并不需要)的导出,webpack会不管三七二十一简单粗暴的将整个模块包含进来,使得我们最终打包的文件里有了许多不需要的垃圾。这就到了tree shaking出手的地方了,因为它能帮助我们干掉那些死代码,大大减少打包的尺寸。
2.使用要求?
1⃣️它依赖于 ES2015 模块系统中的静态结构特性,例如 import
和 export
因此,必须使用ES6模块,不能使用其它类型的模块如CommonJS之流。如果使用Babel的话,这里有一个小问题,因为Babel的预案(preset)默认会将任何模块类型都转译成CommonJS类型。修正这个问题也很简单,不是在.babelrc文件中就是在webpack.config.js文件中设置modules: false就好了
{
"presets": [
[
"@babel/preset-env",
{"modules": false}
],
"@babel/preset-react"
]
}
2⃣️需要使用UglifyJsPlugin插件,如果在mode:"production"模式,这个插件已经默认添加了,如果在其它模式下,可以手工添加它。
optimization: {
usedExports: true,
sideEffects:true,
minimize: true,
minimizer:[new UglifyJsPlugin()]
}
3⃣️在项目 package.json 文件中,添加一个 "sideEffects" 入口。
在一个纯粹的 ESM 模块世界中,识别出哪些文件有副作用很简单。然而,我们的项目无法达到这种纯度,所以,此时有必要向 webpack 的 compiler 提供提示哪些代码是“纯粹部分”。
这种方式是通过 package.json 的 "sideEffects" 属性来实现的。
{
"name": "your-project",
"sideEffects": false
}
如同上面提到的,如果所有代码都不包含副作用,我们就可以简单地将该属性标记为 false,来告知 webpack,它可以安全地删除未用到的 export 导出。
如果你的代码确实有一些副作用,那么可以改为提供一个数组:
{
"name": "your-project",
"sideEffects": [
"./src/some-side-effectful-file.js",
"*.css"
]
}
四、使用 source map
当 webpack 打包源代码时,可能会很难追踪到错误和警告在源代码中的原始位置。例如,如果将三个源文件(a.js
, b.js
和 c.js
)打包到一个 bundle(bundle.js
)中,而其中一个源文件包含一个错误,那么堆栈跟踪就会简单地指向到 bundle.js
。这并通常没有太多帮助,因为你可能需要准确地知道错误来自于哪个源文件。
为了更容易地追踪错误和警告,JavaScript 提供了 source map 功能,将编译后的代码映射回原始源代码。如果一个错误来自于 b.js
,source map 就会明确的告诉你。
source map 有很多不同的选项可用,请务必仔细阅读它们,以便可以根据需要进行配置。
对于本指南,我们使用 inline-source-map
选项,这有助于解释说明我们的目的(仅解释说明,不要用于生产环境):
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
entry: {
app: './src/index.js',
print: './src/print.js'
},
+ devtool: 'inline-source-map',
plugins: [
new CleanWebpackPlugin(['dist']),
new HtmlWebpackPlugin({
title: 'Development'
})
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
现在,让我们来做一些调试,在 Test.js 文件中生成一个错误:
import React, {Component} from 'react'
import './Test.less' ;
class Test extends Component{
render() {
return (
<div className="contianer" onClick={()=>{cosnole.error('I get called from print.js!');}}>
<div className="car car1">1</div>
<div className="car car2">2</div>
<div className="car car3">3</div>
<div className="car car4">4</div>
</div>
)}
}
export default Test
在在浏览器打开最终生成的 index.html 文件,点击按钮,并且在控制台查看显示的错误。错误应该如下
这是非常有帮助的,因为现在我们知道了,所要解决的问题的确切位置。
五、选择一个开发工具
每次要编译代码时,手动运行 npm run build 就会变得很麻烦。
webpack 中有几个不同的选项,可以帮助你在代码发生变化后自动编译代码:
多数场景中,你可能需要使用 webpack-dev-server,但是不妨探讨一下以下的所有选项
1⃣️webpack's Watch Mode
使用观察模式,你可以指示 webpack "watch" 依赖图中的所有文件以进行更改。如果其中一个文件被更新,代码将被重新编译,所以你不必手动运行整个构建。
我们添加一个用于启动 webpack 的观察模式的 npm script 脚本:
package.json
{
"name": "development",
"version": "1.0.0",
"description": "",
"main": "webpack.config.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
+ "watch": "webpack --watch",
"build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"clean-webpack-plugin": "^0.1.16",
"css-loader": "^0.28.4",
"csv-loader": "^2.1.1",
"file-loader": "^0.11.2",
"html-webpack-plugin": "^2.29.0",
"style-loader": "^0.18.2",
"webpack": "^3.0.0",
"xml-loader": "^1.2.1"
}
}
现在,你可以在命令行中运行 npm run watch,就会看到 webpack 编译代码,然而却不会退出命令行。这是因为 script 脚本还在观察文件。现在,修改并保存文件并检查终端窗口。应该可以看到 webpack 自动重新编译修改后的模块!
2⃣️webpack-dev-server
浏览器自动加载页面。如果现在修改和保存任意源文件,web 服务器就会自动重新加载编译后的代码
3⃣️webpack-dev-middleware
webpack-dev-middleware 是一个容器(wrapper),它可以把 webpack 处理后的文件传递给一个服务器(server)。 webpack-dev-server 在内部使用了它,同时,它也可以作为一个单独的包来使用,以便进行更多自定义设置来实现更多的需求。接下来是一个 webpack-dev-middleware 配合 express server 的示例。
npm install --save-dev express webpack-dev-middleware
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
entry: {
app: './src/index.js',
print: './src/print.js'
},
devtool: 'inline-source-map',
plugins: [
new CleanWebpackPlugin(['dist']),
new HtmlWebpackPlugin({
title: 'Output Management'
})
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
+ publicPath: '/'
}
};
server.js
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const app = express();
const config = require('./webpack.config.js');
const compiler = webpack(config);
// Tell express to use the webpack-dev-middleware and use the webpack.config.js
// configuration file as a base.
app.use(webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath
}));
// Serve the files on port 3000.
app.listen(3000, function () {
console.log('Example app listening on port 3000!\n');
});
package.json
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack --mode development --cate",
"build": "webpack --mode production --cate",
"server": "webpack-dev-server --open --cate",
"watch": "webpack --watch --cate",
"express":"node server/server.js --cate"
},
现在,在你的终端执行 npm run express.打开浏览器,跳转到 http://localhost:3000,你应该看到你的webpack 应用程序已经运行!
六、生产环境构建
开发环境(development)和生产环境(production)的构建目标差异很大。在开发环境中,我们需要具有强大的、具有实时重新加载(live reloading)或热模块替换(hot module replacement)能力的 source map 和 localhost server。而在生产环境中,我们的目标则转向于关注更小的 bundle,更轻量的 source map,以及更优化的资源,以改善加载时间。由于要遵循逻辑分离,我们通常建议为每个环境编写彼此独立的 webpack 配置。
虽然,以上我们将生产环境和开发环境做了略微区分,但是,请注意,我们还是会遵循不重复原则(Don't repeat yourself - DRY),保留一个“通用”配置。为了将这些配置合并在一起,我们将使用一个名为 webpack-merge
的工具。通过“通用”配置,我们不必在环境特定(environment-specific)的配置中重复代码。
我们先从安装 webpack-merge
开始:
npm install --save-dev webpack-merge
webpack.common.js
+ const path = require('path');
+ const CleanWebpackPlugin = require('clean-webpack-plugin');
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
+
+ module.exports = {
+ entry: {
+ app: './src/index.js'
+ },
+ plugins: [
+ new CleanWebpackPlugin(['dist']),
+ new HtmlWebpackPlugin({
+ title: 'Production'
+ })
+ ],
+ output: {
+ filename: '[name].bundle.js',
+ path: path.resolve(__dirname, 'dist')
+ }
+ };
webpack.dev.js
+ const merge = require('webpack-merge');
+ const common = require('./webpack.common.js');
+
+ module.exports = merge(common, {
+ devtool: 'inline-source-map',
+ devServer: {
+ contentBase: './dist'
+ }
+ });
webpack.prod.js
+ const merge = require('webpack-merge');
+ const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
+ const common = require('./webpack.common.js');
+
+ module.exports = merge(common, {
+ plugins: [
+ new UglifyJSPlugin()
+ ]
+ });
现在,在 webpack.common.js
中,我们设置了 entry
和 output
配置,并且在其中引入这两个环境公用的全部插件。在 webpack.dev.js
中,我们为此环境添加了推荐的 devtool
(强大的 source map)和简单的 devServer
配置。最后,在 webpack.prod.js
中,我们引入了之前在 tree shaking 指南中介绍过的 UglifyJSPlugin
。
package.json
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack --config webpack.dev.js --cate",
"build": "webpack --config webpack.prod.js --cate",
"server": "webpack-dev-server --open --config webpack.dev.js --cate",
"watch": "webpack --watch --cate",
"express":"node server/server.js --cate"
}
七、外部扩展(externals)
在日常的项目开发中,我们会用到各种第三方库来提高效率,但随之带来的问题就是打包后的js文件体积过大,导致加载时空白页时间过长,给用户的体验太差。
externals 配置选项提供了「从输出的 bundle 中排除依赖」的方法。防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)。
例如,从 CDN 引入 jQuery,而不是把它打包:
webpack.config.js
module.exports = {
//...
externals: {
jquery: 'jQuery'
}
};
index.html
<script
src="https://code.jquery.com/jquery-3.1.0.js"
integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
crossorigin="anonymous">
</script>
八、解析(resolve)
module.exports = {
//...
resolve: {
extensions: ['.js', '.jsx'],
alias: {
page: srcPath+'/page',
components: srcPath+'/components',
images: srcPath+'/images',
mock: srcPath+'/mock',
skin:srcPath+'/skin',
util:srcPath+'/util',
}
};
1⃣️ resolve.alias
创建 import 或 require 的别名,来确保模块引入变得更简单。
现在,替换「在导入时使用相对路径」这种方式,就像这样:
import {add} from '../../util/util'
你可以这样使用别名:
import {add} from 'util/util'
从而使得引用更简单
2⃣️ resolve.extensions
能够使用户在引入模块时不带扩展