首先来简单介绍一下webpack:现代 JavaScript 应用程序的静态模块打包工具。当 webpack 处理应用程序时,它会在内部构建一个会映射项目所需的每一个模块 的依赖图(dependency graph),并生成一个或多个 bundle。webpack4.0出现以后,咱们能够不用再引入一个配置文件来打包项目,而且它仍然有着很高的可配置性,能够很好知足咱们的需求。 在开始正文以前,首先先来看看咱们要实现的成果:javascript
基于webpack4.0搭建的脚手架(支持react/vue/typescript/es6+/jquery+less/scss)css
在脚手架的开发过程当中我会详细介绍每一个插件或者loader的用途以及webpack的核心理念,若有不懂或者有其余更好的想法欢迎交流。 下面是基于该文章的webpack4.0的思惟导图:html
在实现脚手架以前,假设咱们已经建立了目录和package.json文件,接下来先安装webpack相关依赖:前端
// 此处建议安装局部依赖,安装全局依赖可能会出现版本问题
npm install -D webpack webpack-cli
复制代码
由于项目要支持es6+,咱们还须要安装babel相关依赖:vue
npm install -D babel-loader @babel/core @babel/preset-env
复制代码
这个时候能够开始配置咱们的脚手架逻辑了,为了项目结构清晰易于维护,咱们建一个build目录专门放webpack构建的脚本,webpack默认的配置文件是webpack.config.js,因为实际项目须要,咱们将其拆分为3个文件,分别是webpack通用配置文件webpack.base.js,开发环境配置文件webpack.dev.js以及生产环境配置文件webpack.prod.js。 java
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: {
main: './src/index.js',
},
output: {
path: path.resolve(__dirname, '../dist'),
},
module: {
rules: [
// 将es6编译成es5
{
test: /\.jsx?$/, // ?表示x有0个或一个
exclude: /node_modules/, // 不编译某个目录下的文件
include: path.resolve(__dirname, '../src'), // 只在include包含的目录下进行loader编译
use: [
"babel-loader",
]
},
]
}
}
复制代码
为了项目后期的开发和维护,咱们创建好项目结构: node
public目录是事先准备好的html模版,这里就不介绍了,其余目录可根据具体项目进行设置。react
咱们还须要一个插件将打包后的文件植入到html模版中并导出到dist目录下,这里使用html-webpack-plugin来实现jquery
npm install -D html-webpack-plugin
复制代码
如今webpack.base.js为以下:webpack
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const template = path.resolve(__dirname, '../public/index.html');
module.exports = {
entry: {
main: './src/index.js'
},
output: {
path: path.resolve(__dirname, '../dist')
},
module: {
rules: [
// 将es6编译成es5
{
test: /\.jsx?$/, // ?表示x有0个或一个
exclude: /node_modules/, // 不编译某个目录下的文件
include: path.resolve(__dirname, '../src'), // 只在include包含的目录下进行loader编译
use: [
"babel-loader",
]
},
]
},
plugins: [
new HtmlWebpackPlugin({
template,
filename: 'index.html'
})
]
}
复制代码
为了打包项目,咱们须要在webpack.prod.js目录下进行配置,此处须要一个模块webpack-merge将wepack基础配置合并进生产配置,咱们先来安装一下:
npm install -D webpack-merge
复制代码
webpack.prod.js配置以下:
const merge = require('webpack-merge');
const base = require('./webpack.base');
module.exports = merge({
mode: 'production',
output: {
filename: 'js/[name]_[contenthash].js', // 入口和内容hash组成的文件名,也能够是hash
chunkFilename: 'js/[name]_[contenthash].chunk.js'
}
}, base)
复制代码
而后在package.json里添加执行脚本:
"scripts": {
"build": "webpack --config ./build/webpack.prod.js"
}
复制代码
webpack默认会找名为webpack.config.js的文件,因为咱们将其拆解为prod和dev,因此咱们要手动指定webpack执行的文件,添加--config,便可手动指定目录。下面咱们开始写一段代码试试吧,在index.js中写入以下es6代码:
// index.js
let name = 'xuxi';
let say = (name) => {
alert(name)
};
say(name);
复制代码
下面咱们执行:
npm run build
复制代码
此时咱们会看见项目中多了一个dist目录,里面的html也植入了相应的代码,
npm install --save-dev css-loader style-loader
复制代码
在webpack.base.js中的module中添加以下代码:
module: {
rules: [
// 将es6编译成es5
{
test: /\.jsx?$/, // ?表示x有0个或一个
exclude: /node_modules/, // 不编译某个目录下的文件
include: path.resolve(__dirname, '../src'), // 只在include包含的目录下进行loader编译
use: [
"babel-loader",
]
},
// 加载css
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
]
}
复制代码
注意,laoder的加载顺序是从下往上,从右往左的,因此配置loader的时候要注意一下顺序。 此时在styles目录下加入app.css,在js中引入:
// app.css
#root {
background-color: #f0c;
height: 100px;
}
// index.js
import './styles/app.css'
复制代码
此时打开浏览器,能够看到css生效了:
npm isntall mini-css-extract-plugin -D
复制代码
根据该插件的官方配置,咱们须要把style-loader替换成该插件提供的loader,并配置导出的css文件目录和文件名,为了提升开发环境构建速度,咱们只在生产环境分割css:
// webpack.prod.js
const merge = require('webpack-merge');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const base = require('./webpack.base');
module.exports = merge({
mode: 'production',
output: {
filename: 'js/[name]_[contenthash].js', // 入口和内容hash组成的文件名,也能够是hash
chunkFilename: 'js/[name]_[contenthash].chunk.js'
},
module: {
rules: [
{
test: /\.css$/,
use: [ // loader解析的顺序是从下到上,从右到左的顺序
{
loader: MiniCssExtractPlugin.loader,
options: {
filename: '[name].css',
chunkFilename: '[name].css',
publicPath: '../' //****最后打包的时候替换引入文件路径
},
},
// 'style-loader', 使用MiniCssExtractPlugin时就不能使用style-loader了
{
loader: 'css-loader',
options: {
importLoaders: 2 //该方式可让@import引入的css文件再次执行一边css打包loader
}
},
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: 'css/[name]_[hash].css',
chunkFilename: 'css/[name]_[hash].chunk.css',
}),
]
}, base)
复制代码
因为咱们在dev和prod环境的css-loader不同,因此咱们将base的css-loader配置删除,移到dev下
// webpack.dev.js
const base = require('./webpack.base');
const merge = require('webpack-merge');
const webpack = require('webpack');
module.exports = merge({
mode: 'development',
module: {
rules: [
{
test: /\.css$/,
use: [ // loader解析的顺序是从下到上,从右到左的顺序
'style-loader', //使用MiniCssExtractPlugin时就不能使用style-loader了
{
loader: 'css-loader',
options: {
importLoaders: 2 //该方式可让@import引入的css文件再次执行一边css打包loader
}
},
]
}
]
},
output: {
filename: '[name].js',
chunkFilename: '[name].js',
}
}, base)
复制代码
ok,此时咱们就完成一个基本的打包es6,css的模块打包工具,为了支持更高的es6+语法,咱们配置.babelrc文件,以及安装相应的npm包:
npm install @babel/polyfill core-js -D
复制代码
.babelrc文件以下:
{
"presets": [
[
"@babel/preset-env", // 将ES6语法转换为es5
{
"useBuiltIns": "usage", // 只编译须要编译的代码
"corejs": "3.0.1",
}
],
]
}
复制代码
咱们会看到babelrc文件里面有"useBuiltIns": "usage", 这个配置涉及到一个关于webpack打包的高级用法,tree-shaking。
tree-shaking:用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块语法的 静态结构 特性,例如
import
和export
。这个术语和概念其实是由 ES2015 模块打包工具 rollup 普及起来的。
咱们经过使用tree-shaking,能够极大的减小代码的体积,对于提升打包性能颇有帮助。为了使tree-shaking能生效,咱们还要在webpack配置文件中开启:
// webpack.base.js
optimization: {
usedExports: true
}
复制代码
还有一个问题,因为tree-shaking是基于import 和export的,当咱们用import引入css文件时,是没有导出的,因此咱们须要配置忽略css文件的tree-shaking,在package.json中添加以下配置:
// package.json
"sideEffects": [
"*.css",
"*.less"
],
复制代码
在打包的过程当中,每次执行打包都会新建一个打包文件,咱们想要每次打包以前都清除上一次打包的文件怎么办呢?咱们可使用clean-webpack-plugin来实现,首先先安装,而后具体配置以下:
// webpack.prod.js
plugins: [
new CleanWebpackPlugin()
],
复制代码
该插件会默认清除dist目录下的打包文件。 接下来咱们安装jquery:
npm install jquery -S
复制代码
在index.js引入并使用:
import $ from 'jquery';
import './styles/app.css'
console.log($('#root').html('hello world'));
复制代码
执行npm run build后,咱们在浏览器中打开,便可看到jq的做用:
//webpack.base.js
optimization: {
splitChunks: {
chunks: 'all',
// chunks: 'async', // async表示只对异步代码进行分割
minSize: 30000, // 当超过指定大小时作代码分割
// maxSize: 200000, // 当大于最大尺寸时对代码进行二次分割
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '_',
name: true,
cacheGroups: { // 缓存组:若是知足vendor的条件,就按vender打包,不然按default打包
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10, // 权重越大,打包优先级越高
// filename: 'js/vender.js' //将代码打包成名为vender.js的文件
name: 'vender'
},
default: {
minChunks: 2,
priority: -20,
name: 'common',
// filename: 'js/common.js',
reuseExistingChunk: true // 是否复用已经打包过的代码
}
}
},
usedExports: true
}
复制代码
因为篇幅限制,splitChunks的具体配置我经过注释写出来了,若是想了解更详细的配置,后面你们能够去webpack官网查看。此时执行npm run build,咱们能够看到代码已经进行了分割:
npm install url-loader file-loader less less-loader
复制代码
此时咱们webpack.base.js中module变为:
module: {
rules: [
// 将es6编译成es5
{
test: /\.jsx?$/, // ?表示x有0个或一个
exclude: /node_modules/, // 不编译某个目录下的文件
include: path.resolve(__dirname, '../src'), // 只在include包含的目录下进行loader编译
use: [
"babel-loader",
]
},
// 加载解析文件资源
{
test: /\.(jpg|png|gif)$/,
use: {
loader: 'url-loader', // 和file-loader功能相同,但更智能
options: {
// 配置打包后的文件名,具体可看webpack的file-loader文档
name: '[name].[ext]?[hash]',
outputPath: 'images/',
limit: 4096 // 当图片大小大于4k时将以文件形式输出,不然以base64输出
}
}
},
// 引入字体,svg等文件
{
test: /\.(eot|ttf|svg)$/,
use: {
loader: 'file-loader'
}
}
]
},
复制代码
为了支持less,咱们修改一下dev和prod文件:
// webpack.dev.js
module: {
rules: [
{
test: /\.(css|less)$/,
use: [ // loader解析的顺序是从下到上,从右到左的顺序
'style-loader', //使用MiniCssExtractPlugin时就不能使用style-loader了
{
loader: 'css-loader',
options: {
importLoaders: 2 //该方式可让@import引入的css文件再次执行一边css打包loader
}
},
'less-loader',
]
}
]
}
// webpack.prod.js
module: {
rules: [
{
test: /\.(css|less)$/,
use: [ // loader解析的顺序是从下到上,从右到左的顺序
{
loader: MiniCssExtractPlugin.loader,
options: {
filename: '[name].css',
chunkFilename: '[name].css',
publicPath: '../' //****最后打包的时候替换引入文件路径
},
},
{
loader: 'css-loader',
options: {
importLoaders: 2 //该方式可让@import引入的css文件再次执行一边css打包loader
}
},
'less-loader'
]
}
]
}
复制代码
咱们写一段less代码试试:
body {
#root {
background-color: #000;
color: #fff;
}
}
复制代码
咱们执行build以后在浏览器打开看看效果:
npm install webpack-dev-server -D
复制代码
接着配置dev文件:
webpack.dev.js
const base = require('./webpack.base');
const merge = require('webpack-merge');
const webpack = require('webpack');
module.exports = merge({
mode: 'development',
module: {
rules: [
{
test: /\.(css|less)$/,
use: [ // loader解析的顺序是从下到上,从右到左的顺序
'style-loader', //使用MiniCssExtractPlugin时就不能使用style-loader了
'vue-style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2 //该方式可让@import引入的css文件再次执行一边css打包loader
}
},
// 'sass-loader',
'less-loader',
'postcss-loader',
]
}
]
},
// 服务器配置
devServer: {
port: '8081',
// 当使用 HTML5 History API 时,任意的 404 响应均可能须要被替代为 index.html
historyApiFallback: true, // 解决单页面路由问题,
contentBase: '../dist',
open: true, //自动打开浏览器
hot: true, // 开启热替换, css代码跟新不刷新页面
// hotOnly: true 开启后只有手动配置才能更新,即便hot为true也不刷新浏览器
proxy: {
index: '', // 将index设置为空,能够对根路径进行转发
'api/get': 'xxxx.com/api', // 第一种方式,直接代理到api路径
'api/vue': { // 第二种方式,在路径须要临时替换时使用
target: 'xxxx.com/api',
pathRewrite: {
'head': 'demo' //此时访问head路径将被代理到demo下
},
secure: false, //对https请求的配置,false为支持https
changeOrigin: true //作代理分发时容许访问其余网站,突破网站限制,建议在开发环境使用
},
}
},
plugins: [
new webpack.HotModuleReplacementPlugin()
],
output: {
filename: '[name].js',
chunkFilename: '[name].js',
}
}, base)
复制代码
以上代码中,要使用热模块替换,咱们须要用到webpack本身集成的插件webpack.HotModuleReplacementPlugin,在devServer中,咱们还能够设置开发环境中的代理proxy,这已是目前开发的默认模式了,代码中一些属性的用法和含义我都作了注释,若是你们有兴趣,能够查看webpack原版官方文档,那里有更详细的配置信息。 咱们再来修改package.json,添加开发环境的运行指令:
// 在script里面添加
"start": "webpack-dev-server --config ./build/webpack.dev.js",
复制代码
咱们执行 npm start,此时会自动打开浏览器,运行咱们的项目。
到此,咱们基本的一个支持ES6+Less/css+JQuery的单页应用打包工具已经作好了,固然这只是基础,后面的多页应用,vue/react/typescript都是在这个基础上构建的,让咱们拭目以待。
咱们开发多页面应用仍是须要用到以前使用的html-webpack-plugin插件,此时咱们须要定义多个入口:
// webpack.base.js
entry: {
main: './src/index.js',
about: './src/about.js'
}
plugins: [
new HtmlWebpackPlugin(
{
template,
title: 'webpack打包但单应用',
chunks: ['vender', 'main'],
filename: 'index.html'
}
),
new HtmlWebpackPlugin(
{
template,
title: '关于咱们',
chunks: ['vender', 'about'],
filename: 'about.html'
}
),
]
复制代码
template是咱们定义的public下的html路径,title是咱们要植入html模版中的titl标签中的内容,咱们在index.html中这么使用:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title><%= htmlWebpackPlugin.options.title %></title>
<link rel="shortcut icon" href="./favicon.ico">
</head>
<body>
<div id="root"></div>
</body>
</html>
复制代码
这样,webpack配置的title就能够动态的添加到html页面中了,这里我要说一下在new HtmlWebpackPlugin中咱们添加了chunks数组,这个数组就是咱们要打包进页面的js,main和about分别表明入口的名字,vender就是咱们定义optimization时里面cacheGroups属性值中vendors中定义的name,当代码超过30000b时就会生成vender.js文件。 此时咱们执行npm run build,打包结束后将会生成2个html页面,对应的文件依赖也会引入,在浏览器中打开,亲测有效~
这样,一个基本的多页面打包工具就开发完成了,不过还有几点优化:
npm install terser-webpack-plugin optimize-css-assets-webpack-plugin -D
复制代码
咱们在webpack.prod.js中添加一下配置:
// 导入模块
// 压缩css
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
// 压缩js
const TerserPlugin = require('terser-webpack-plugin');
// 配置module.exports,添加配置以下
optimization: {
minimizer: [
new TerserPlugin({ // 压缩js代码
cache: true, // 启用文件缓存
parallel: true, // 使用多进程并行执行任务来提升构建效率
sourceMap: true, // 将错误消息位置映射到模块
terserOptions: {
drop_console: true, // 打包时剔除全部console.log
drop_debugger: true // 打包时剔除全部debugger
}
}),
new OptimizeCSSAssetsPlugin({})] // 压缩css代码
},
复制代码
npm install workbox-webpack-plugin --save-dev
复制代码
在须要作pwa的页面里加入以下脚本启动:
if ('serviceWorker' in navigator) { window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js').then(registration => {
console.log('SW registered: ', registration);
}).catch(registrationError => {
console.log('SW registration failed: ', registrationError);
});
});
}
复制代码
至此,咱们关于开发基于ES6+JQuery+Less/Scss的单页/多页脚手架就告于段落了,下面咱们来集成对react/vue/typescript的支持。
咱们首先安装一个babel模块:
npm install --save-dev @babel/preset-react
复制代码
而后在.babelrc中加入以下配置:
{
"presets": [
[
"@babel/preset-react",
{
"pragma": "dom", // default pragma is React.createElement
"pragmaFrag": "DomFrag", // default is React.Fragment
"throwIfNamespace": false // defaults to true
}
]
]
}
复制代码
而后在index.js中写入一段react代码:
import React, {Component} from 'react'
import ReactDOM from 'react-dom'
class App extends Component {
render() {
return <div>react frame content.</div>
}
}
ReactDOM.render(<App />, document.getElementById('root')); 复制代码
打包后便可在浏览器中看到效果。
首先先安装对应npm包:
npm install -D vue-loader vue-template-compiler
复制代码
以后在webpack的配置文件中写入以下代码:
// webpack.base.js
const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
module: {
rules: [
// ... other rules
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
},
plugins: [
// make sure to include the plugin!
new VueLoaderPlugin()
]
}
复制代码
若是要想解析.vue文件中的style,咱们须要使用vue-style-loader模块,安装以后将其添加到loder中便可。 接下来咱们写个简单的vue试一下吧:
<template>
<div class="example"> {{ msg }} <img src="~/images/logo.png" /> <img :src="imgSrc" /> </div> </template> <script> import Logo from 'images/logo.png'; import 'css/common.css'; export default { data () { return { msg: 'Hello world!单独的', imgSrc: Logo } } } </script> <style lang="less"> .example { color: red; img { border: 2px solid #000; } } </style> 复制代码
运行dev在浏览器中便可看到效果。 值得注意的是vue文件中引入资源的问题,使用相对路径会有问题,这里咱们可使用~/images/logo.png的方式或者require的方式来引入图片。
这里咱们使用awesome-typescript-loader来编译typescript文件,也是官方推荐的一个加载器:
npm install awesome-typescript-loader --save-dev
复制代码
而后咱们在webpack的配置文件base中,在module的rules里加入以下代码:
{
test: /\.tsx?$/,
loader: 'awesome-typescript-loader'
}
复制代码
最后一步,添加tsconfig.json文件:
{
"compilerOptions": {
"noImplicitAny": true,
"removeComments": true
},
"awesomeTypescriptLoaderOptions": {
/* ... */
}
}
复制代码
该文件有不少灵活的配置项,你们若是想了解更多能够去typescript官网上查看相关文档。 至此,全部的配置都完成了,是否是很累?哈哈,固然脚手架中还存在一些优化的地方,欢迎你们能够一块儿完善。
最后,欢迎加入前端技术群,一块儿探讨前端的魅力: