所有能用JavaScript实现的,最终都将用JavaScript实现
前言
为了熟悉webpack
配置,每次重新开始一个新项目时我都会手动自己搭建开发环境,一步步的加深记忆,也有好处就是慢慢形成自己的开发风格.但是久而久之我开始厌倦了每次都只是改配置,有时候一些操作我希望用函数式操作
直到我看到了webpack-chain
我知道我拿到了属于我的金箍棒了.
关于webpack-chain
可以看看官方说明
webpack-chain项目中文翻译
webpack 的核心配置的创建和修改基于一个有潜在难于处理的 JavaScript 对象。虽然这对于配置单个项目来说还是 OK 的,但当你尝试跨项目共享这些对象并使其进行后续的修改就会变的混乱不堪,因为您需要深入了解底层对象的结构以进行这些更改。
前两篇写了一些基础配置,对于webpack
还不是很熟悉的可以看看
Webpack4.X重修之路 --- 基础篇
Webpack4.X重修之路 --- 样式篇
先简单说一下webpack-chain
的使用
// 导入 webpack-chain 模块,该模块导出了一个用于创建一个webpack配置API的单一构造函数。
const Config = require('webpack-chain');
// 对该单一构造函数创建一个新的配置实例
const config = new Config();
// 用链式API改变配置
// 每个API的调用都会跟踪对存储配置的更改。
config
// 修改 entry 配置
.entry('index')
.add('src/index.js')
.end()
// 修改 output 配置
.output
.path('dist')
.filename('[name].bundle.js');
开始之前
下面讲的目录结构都是跟前两两篇说明是一样,不一样的是我们在根目录新增一个scripts
目录用于存放脚本文件
├── base.js
├── config.js
├── dev.js
├── loaders.js
├── prod.js
├── utils.js
文件说明
-
base.js
: 开发模式以及生产模式共同的配置,入口&出口&HTML模板&需要复制的文件夹 -
config.js
: 配置文件,对于不同的项目需求,只需要修改这里的参数即可 -
dev.js
: 开发模式的配置 -
prod.js
生产模式的配置 -
loaders
:webpack
中module
和plugins
的配置 -
utils.js
: 工具函数
config.js
对于config.js
我的打算时我想修改什么配置时,比如使用vue
,vue-router
或者ts
等只需要在这里修改对应参数就可以.
// 路径都是以当前路径为准
// 路径都需要是相对路径
module.exports = {
base: {
// 入口文件
entryDir: '../src/index.ts',
// 发布路径
outputDir: '../dist',
// 全局不打包文件目录,需配置默认html模板使用
globalConfig: {
source: '../src/assets/config/',
targetDir: 'config'
}
// html文件模板路径
// 自定义html模板时,如果使用全局不打包的js需要手动插入
// html: string
// devServer配置
server: {
port: 3333,
host: '0.0.0.0',
// filename,
// proxy
}
},
// less设置
less: {
// 是否引入less-plugin-functions
lessFunction: true,
// common less file 公共less文件,不用引入即可使用
// 不需要时设置为false
lessCommon: '../src/styles/common.less'
},
// vue options
vue: {
// 是否使用vue
open: true,
// 是否在vue中使用typescript
withTS: true,
// vue中使用的框架,打包时会分割出来
// 优先级按先后顺序
libs: ['vue', 'vue-property-decorator']
},
// 是否使用typescript
// ts: false,
// 待实现
// react
// ...
}
utlis.js
const path = require('path');
// 路径处理
module.exports = {
resolve(fileDir) {
return path.resolve(__dirname, fileDir);
}
}
base.js
base.js
导出一个匿名函数,接收webpack-chain
的配置对象,在被dev.js
和prod.js
中引入使用,loaders
也是这样设计的
- 设置webpack入口出口配置
webpackConfig.entry(name).add(file).end().output.path(path).filename
- 设置webpack插件
webpackConfig.plugin(name).use(插件, [ 插件的设置 ])
module.exports = webpackConfig => { // todo... }
- 定义入口文件以及出口设置
ps:这里是单页面,多页面的配置是差不多的
// base entry & output
webpackConfig
.entry('app')
.add(entryDir)
.end()
.output
.path(outputDir)
.filename('js/[name].bundle.js');
设置
html
模板
使用html-webpack-plugin
插件来进行html
文件处理
使用html-webpack-temp
生成html
模板复制
assets
文件夹,会将此目录下所有文件夹移至打包后根目录
使用copy-webpack-plugin
loaders.js
创建webpack.module.rules
,对于不同的环境以及不同的使用插件使用不同的配置,这里参考了vue-cli
的一些设置
样式: css
和less
的loader
, 包括在css分离
,使用Less自定义函数
, 使用Less
全局变量以及生产模式下自动添加后缀
,压缩
等;使用的包:
- mini-css-extract-plugin
- css-loader
- style-loader
- postcss-loader
- autoprefixer
- less
- less-loader
- less-plugin-functions
- style-resources-loader
JS: vue
,ts
等开发环境,包括代码分离,生产模式下压缩混淆等. 使用的包:
- babel-loader
- vue-loader
- ts-loader
我将webpack
完成了gulp
,对于引入的框架,我希望可以单独打包,下面是实现代码
function addEntry(name, module) {
webpackConfig
.entry(name)
.add(module)
.end()
}
// 分割引入的vue框架
const otherVuelib = config.vue.libs.filter( lib => {
return lib === 'vue' || 'vue-property-decorator'
})
if(otherVuelib.length > 0) {
otherVuelib.forEach( lib => {
addEntry(lib, lib)
})
}
webpackConfig.optimization
.splitChunks({
chunks: 'all',
minSize: 30000,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '-',
name: true,
cacheGroups: createVueCacheGroups(config.vue.libs)
})
function createVueCacheGroups(libs) {
let groups = {};
libs.forEach( (lib, index) => {
groups[lib] = {
test: '/[\\/]node_modules[\\/]' + lib + '[\\/]/',
priority: -10 * (index + 1),
name: lib
}
})
return groups;
}
dev.js
设置webpack.mode
,和devServer
,并将webpack-chain
配置导出为webpack
认识的配置
const Config = require('webpack-chain');
const webpackConfig = new Config();
const base = require('./base');
const loaders = require('./loaders');
const options = require('./config');
webpackConfig
.mode('development')
base(webpackConfig);
loaders(webpackConfig);
// devServer
const {
port = 8080,
host = 'localhost',
filename = 'index.html',
https,
proxy,
} = options.base.server;
webpackConfig.devServer
.port(port)
.host(host)
.filename(filename)
.proxy(proxy)
.https(https)
module.exports = webpackConfig.toConfig();
prod.js
const Config = require('webpack-chain');
const webpackConfig = new Config();
const base = require('./base');
const loaders = require('./loaders');
webpackConfig
.mode('production')
base(webpackConfig)
loaders(webpackConfig)
module.exports = webpackConfig.toConfig();
更新
添加了多页的配置,已经放到github
仓库上.
实现多页
首先看改动的配置文件
// config.js
...
// 全局不打包文件目录,需配置默认html模板使用
// 自定义html模板时,如果使用全局不打包的js需要手动插入
globalConfig: {
source: '../src/assets/config/',
targetDir: 'config',
// 需要在多页情况下使用: array
chunks: ['index', 'login']
},
// 多页
multiPages: {
index: {
entry: '../src/index.ts',
// 不需要的模块
excludeChunks: ['login']
// html:
},
login: {
entry: '../src/login.ts',
excludeChunks: ['index']
// html: '../src/login.html',
}
},
原理其实很简单,就是使用webpackConfig
多添加几个入口文件,难点主要在html-webpack-plugin
,之前说过我是使用html-webpack-template
用于生成默认的html模板的,所以多页的情况下需要手动指定每个生成的html
文件的filename
以及需要引入的chunks
// base.js
...
for(let key in CONFIG.base.multiPages){
webpackConfig
.entry(key)
.add( api.resolve(CONFIG.base.multiPages[key].entry) )
.end()
}
...
由于我将框架进行了拆分, 无法显式的导入模块,只能配置每个页面的excludeChunks
则不需要加载的模块. 还在globalConfig
中定义需要插入不打包全局的js
文件.
// base.js
const htmlOption = {};
const htmlOptions = [];
const htmlMergeOptions = {
inject: false,
appMountId: 'app',
meta: [{
name: 'viewport',
content: 'width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0'
}],
favicon: path.resolve(__dirname, '../favicon.ico')
}
const isProdHtml = {
minify: isProd ? {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true,
collapseBooleanAttributes: true,
removeScriptTypeAttributes: true
// more CONFIG:
// https://github.com/kangax/html-minifier#CONFIG-quick-reference
} : {}
}
const multiPages = CONFIG.base.multiPages;
for(let key in multiPages) {
let htmlOption = {};
if( !multiPages[key].html ) {
htmlOption.template = require('html-webpack-template');
}else {
htmlOption.template = api.resolve(multiPages[key].html);
}
let isNeedScript = false;
if( CONFIG.base.globalConfig.chunks.includes(key) ){
isNeedScript = true;
}
Object.assign(htmlOption, Object.assign(multiPages[key].html ? {} : htmlMergeOptions, {
excludeChunks: multiPages[key].excludeChunks,
filename: `${key}.html`
}), isProdHtml, isNeedScript ? {
headHtmlSnippet: isGlobalConfig ? getConfigScript(isGlobalConfig, `${targetDir}/`) : undefined
} : { headHtmlSnippet: undefined } )
htmlOptions.push(htmlOption);
}
let _i = 0;
for(let _page in multiPages) {
webpackConfig
.plugin(_page)
.use(HtmlWebpackPlugin, [ htmlOptions[_i++] ]);
}
实现文件打包与图片压缩
使用的包
url-loader
使用的插件
imagemin-webpack-plugin
新建一个files.js
module.exports = webpackConfig => {
webpackConfig.module.rule()
.test(/\.(jpe?g|png|gif|svg|woff|woff2|eot|ttf)$/)
.use('url-loader')
.loader('url-loader')
.options({
limit: 8192,
name: 'img/[name].[ext]'
})
.end()
const imageMinWebpackPlugin = require('imagemin-webpack-plugin').default;
webpackConfig
.plugin('imagemin')
.use(imageMinWebpackPlugin, [{
test: /\.(jpe?g|png|gif|svg)$/i,
disable: webpackConfig.get('mode') !== 'production',
pngquant: {
quality: '60-80'
}
}])
.end()
.plugin('default-imagemin')
.use(imageMinWebpackPlugin, [{test: '../src/assets/img/**'}])
}
更新
React
- 安装
react
react-dom
@babel/preset-react
@babel/core
@babel/preset-env
依然使用babel-loader
处理/\.jsx?/
文件,修改options
{"presets": [ "@babel/preset-env", "@babel/preset-react" ]}
React + TypeScript
在已经支持
react
的基础上
- 安装
- @types/react
- @types/react-dom
awesome-typescript-loader
source-map-loader
createJSRule('tsx', /\.tsx?$/, 'awesome-typescript-loader')
createJSRule('js', /\.js$/, 'source-map-loader', {}, [], 'pre');
PS:当定义有react组件的文件需以tsx为后缀,否则TypeScript会报错