Webpack4.X重修之路 --- 链式配置篇

所有能用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
文件说明
  1. base.js: 开发模式以及生产模式共同的配置,入口&出口&HTML模板&需要复制的文件夹
  2. config.js: 配置文件,对于不同的项目需求,只需要修改这里的参数即可
  3. dev.js: 开发模式的配置
  4. prod.js 生产模式的配置
  5. loaders: webpackmoduleplugins的配置
  6. 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.jsprod.js中引入使用,loaders也是这样设计的

  • 设置webpack入口出口配置

webpackConfig.entry(name).add(file).end().output.path(path).filename

  • 设置webpack插件

webpackConfig.plugin(name).use(插件, [ 插件的设置 ])

module.exports = webpackConfig => {  // todo... }
  1. 定义入口文件以及出口设置
    ps:这里是单页面,多页面的配置是差不多的
    // base entry & output
    webpackConfig
        .entry('app')
        .add(entryDir)
        .end()
        .output
        .path(outputDir)
        .filename('js/[name].bundle.js');
  1. 设置html模板
    使用html-webpack-plugin插件来进行html文件处理
    使用html-webpack-temp生成html模板

  2. 复制assets文件夹,会将此目录下所有文件夹移至打包后根目录
    使用copy-webpack-plugin

loaders.js

创建webpack.module.rules,对于不同的环境以及不同的使用插件使用不同的配置,这里参考了vue-cli的一些设置

样式: csslessloader, 包括在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

  • 安装
  1. react
  2. react-dom
  3. @babel/preset-react
  4. @babel/core
  5. @babel/preset-env

依然使用babel-loader处理/\.jsx?/文件,修改options

{"presets": [ "@babel/preset-env", "@babel/preset-react" ]} 

React + TypeScript

在已经支持react的基础上

  • 安装
  1. @types/react
  2. @types/react-dom
  3. awesome-typescript-loader
  4. source-map-loader
createJSRule('tsx', /\.tsx?$/, 'awesome-typescript-loader')
createJSRule('js', /\.js$/, 'source-map-loader', {}, [], 'pre');

PS:当定义有react组件的文件需以tsx为后缀,否则TypeScript会报错

参考文档:

webpack-chain项目中文翻译

webpack-chain

webpack中文文档

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343