被女友手把手入门webpack

2,472 阅读11分钟

为什么要构建?

前端发展好快的。

越来越多的思想和框架都出来了。

就是为了提高开发的效率。

比如ES6需要通过babel转成ES5才能在浏览器上面运行吧。

比如SCSS需要转换成css才能在浏览器运行吧

...

这些思想和框架,都是有构建需求的。

为什么要选择webpack来做构建?

webpack 把一切都当成模块!

webpack可以通过plugin来拓展功能!

webpack的社区非常的庞大活跃,紧跟着时代!生态链完整,维护性也很高!

想要理解为什么要使用 webpack,我们先回顾下历史,在打包工具出现之前,我们是如何在 web 中使用 JavaScript 的。 在浏览器中运行 JavaScript 有两种方法。第一种方式,引用一些脚本来存放每个功能;此解决方案很难扩展,因为加载太多脚本会导致网络瓶颈。第二种方式,使用一个包含所有项目代码的大型 .js 文件,但是这会导致作用域、文件大小、可读性和可维护性方面的问题。

为什么选择 webpack : webpack.docschina.org/concepts/wh…

webpack初次打包

webpack是一个模块打包工具,但是只能打包js,所以如果要打包其他的比如css,图片之类的模块,还需要借助loader,loader不能够解决的问题还需要借助插件plugin来拓展功能。

安装

yarn add webpack webpack-cli

配置

  1. 创建webpack.config.js文件
  2. 配置webpack.config.js
    const path = require('path')
    
    module.exports={
        entry: {
            main:'./src/index.js'
        },
        output: {
            path: path.resolve(__dirname, 'dist'),
            filename: "[name].js"
        }
    }
    
  3. 配置packge.json
    "script":{
        "start":"webpack"
    }
    

解释配置

其实webpack里面已经默认配置了很多非常丰富的东西,比如output,如果我们不配置output的话,默认就会输出一个dist文件夹,里面有着名叫bundle.js的文件。

我们也可以自己配置:

  1. entry

    入口文件的配置,如果只有一个入口文件,我们可以entry:'./src/index.js'

    如果有多个入口文件,或者你想要给你的入口文件指定一个名字:里面的main就是入口文件的名字。

    这个入口文件的名字有一个好处就是,后面很多配置可能都会用到:我们不需要再手动输入main, 仅仅是使用'[name]'就可以锁定到之前配置的main

    这样就可以实现,改一处,便可改所有。

  2. output

    path是你输出的时候,会生成一个文件夹,文件夹里面有一个文件filename,文件的名字叫做'[name].js'。我们上面提到的,这个[name]就对应着入口文件名!

loader

因为webpack只能够打包解析js代码,所以面对非js的模块,我们要用loader来解析!

css

安装

yarn add style-loader css-loader

配置

module.exports={
    module: {
        rules: [
            {
                test:/\.css$/,
                loaders:['style-loader','css-loader']
            },
        ]
    }
}    

解释配置

test:/\.css$/:当我们遇到以.css结尾的文件,我们就走下面的loader。

css-loader将css的文件集合在一起,然后由style-loader将css代码转换成字符串插入到我们的输出文件main.js里面。

scss

css已经不能满足我们了,我们要用功能更强大的scss!

安装

yarn add sass-loader style-loader css-loader node-sass

配置

module.exports={
    module: {
        rules: [
            {
                test:/\.scss$/,
                loaders:['style-loader','css-loader','sass-loader']
            },
        ]
    }
}    

解释配置

sass-loader先将scss代码编译成css代码,css-loader将css的文件集合在一起,然后由style-loader将css代码转换成字符串插入到我们的输出文件main.js里面。

image

安装

yarn add file-loader url-loader

配置

module.exports={
    module: {
        rules: [
           {
                test:/\.(jpg|png|jpeg)$/,
                use: {
                    loader: "url-loader",
                    options: {
                        outputPath:'images',
                        name:'[name].[ext]',
                        limit: 2048
                    }
                }
        ]
    }
}    

解释配置

我们遇到jpg png jpeg结尾的,我们就走下面的配置!

如果我的图片大小是大于limit: 2048 2kb的,我就在dist目录下创建一个images的文件夹,文件夹里面放我用file-loader打包的图片,名字是'[name].[ext]',[name]就是我配置的入口文件名,.[ext]我们的图片后缀。

如果我的图片大小是小于2kb的,我就用url-loader,url-loader会将图片转换成base64,插入在main.js里面。

小图片的base64转换是没有意义的,因为小图片被base64转换了之后,大小反而变得更大了点。

plugin

plugin是用来拓展webpack的功能的!

html-webpack-plugin

安装

yarn add html-webpack-plugin

配置

const HtmlPlugin = require('html-webpack-plugin')

plugins:[
  new HtmlPlugin({
         template: "index.html"
    })
]

解释配置

template: "index.html"用我们当前目录下的index.html文件作为模版

在打包之后生成的dist文件夹里面生产一个同样模版的index.html文件。

clean-webpack-plugin

安装

yarn add clean-webpack-plugin

配置

const {CleanWebpackPlugin} = require('clean-webpack-plugin')

plugins:[
 new CleanWebpackPlugin({})
]

解释配置

每次打包之前先删除dist文件夹!

watch

在开发的时候,每次修改代码都要自己去重新打包,预览。

这样真的好麻烦!😓

我们期望,如果可以我一修改代码,代码就自己自动重新构建,然后页面马上也跟着刷新了!

提升工作效率,优化开发体验

安装

配置

"script":{
    "start":"webpack --watch"
}

解释配置

执行npm start,运行index.html,我们的文件就处于监听中!

当我们修改了代码之后,在浏览器,重新手动刷新一下页面,我们就可以看到最新的修改了!

devServer

每次修改了代码之后,还要手动重新刷新一下浏览器,太麻烦了!

而且,文件系统不能发ajax请求!这是一个让人头大的问题!

安装

yarn add webpack-dev-server

配置

devServer: {
        contentBase:'./dist',
        hot:true,
        hotOnly:true, 
        open:true,
        historyApiFallback: true,
        overlay:true,
        progress: true
    }
"script":{
    "start":"webpack-dev-server"
}

解释配置

webpack-dev-server只能在开发环境下面用!

devServer里面的配置也可以换一种方式写,写到script上面"start":"webpack-dev-server --hot --open"

当然,devServer里面很多配置都是默认自带的:

  1. contentBase:'./dist':在dist目录下,开启服务器
  2. hot:true开启热更新模式!当你修改了代码,你再也不用手动刷新页面了,浏览器会自动帮忙刷新!
  3. hotOnly:true:即使HMR不生效,浏览器也不自动刷新
  4. historyApiFallback: true:如果我们的页面发生404了,就会去index.html页面,而不是直接抛一个错误页面
  5. open:true:当我们打包完成,自动打开浏览器,自动加载我们的index.html页面
  6. overlay:true:如果代码发生了错误,直接把错误情况显示在浏览器的页面上!
  7. progress: true:显示你打包的进程

注意! 如果css代码已经从main.js里面分离出来成为一个css文件了,那么css代码的热加载是不起作用的!

HMR

虽然我们有了一个特别好的 webpack-dev-server --hot

但是hot功能,每次自动刷新浏览器的时候,都是加载全部的资源!就是相当于重新刷新了一次页面!

但是,我们希望,假如:如果我们只修改了css文件,那么就重新加载css文件好了!

只替换我们更新的模块!

Hot Module Replacement = HMR

安装

配置

const webpack = require('webpack')

plugins: [
   new webpack.HotModuleReplacementPlugin()
]

在我们的index.js入口文件里面再塞一个:

if (module.hot) {
    module.hot.accept();
}

解释配置

  1. HotModuleReplacementPlugin可以实现热模块的更新,当我们更新了代码的时候,浏览器network加载我们生成的hot.update的js和json文件。而不是之前的所有资源都再重新加载一次!
  2. 我们必须在某个文件接受module.hot.accept(),如果没有文件接受,就不会生成热模块替换的文件。
  3. 为什么我们的css不需要写module.hot.accept(),是因为css-loader已经为我们完成了这一项操作
  4. 我们可以在module.hot监听是哪个文件发生了修改,再做自己想做的操作:
    if (module.hot) {
        console.log('修改了...')
        module.hot.accept('@components/child', () => {
            ReactDom.render( <App/>,document.getElementById('root'))
        });
    }
    
    比如我监听到了'@components/child'这个文件发生了修改,那么我就重新render一下页面!

jsx

安装

yarn add babel-loader

配置

 {
    test:/\.(js|jsx)$/,
    exclude:/node_modules/,
    loader: 'babel-loader'
}

解释配置

排除node_modules文件夹下的,以jsjsx结尾的文件,我们要用babel-loader将es6的代码转换成es5的!

tsx

安装

yarn add awesome-typescript-loader

配置

{
    test:/\.(ts)x$/,
    exclude:/node_modules/,
    loader: "awesome-typescript-loader"
}

解释配置

排除node_modules文件夹下的,以tstsx结尾的文件,我们要用awesome-typescript-loader将ts代码转换成可以编译的js代码

react

安装

yarn add react react-dom @babel/preset-env @babel/preset-react

配置

创建.babelrc文件

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

解释配置

配置好了就可以用react了....

resolve为代码的引入带来的方便

安装

配置

resolve: {
        extensions: ['.js','.jsx','.tsx'],
        mainFiles: ['index'],
        alias: {
            '@components':path.resolve(__dirname, 'src/components'),
            '@pages': path.resolve(__dirname, 'src/pages'),
            '@assets': path.resolve(__dirname, 'src/assets')
        }
    }

解释配置

  1. extensions: ['.js','.jsx','.tsx']: 以js,jsx,tsx文件结尾的,我们在import的时候,可以不用写后缀!
  2. mainFiles: ['index']:如果这个文件叫做index,那么我们可以不用写文件名,直接import上一级的文件夹名就可以了
  3. alias:我们做import引入的时候,如果我们改变了文件的路径,那么引入的路径也要改,路径改很麻烦,所以我们使用alias。如果引入路径为src/components,我们可以直接用@components代替!

动态chunk

安装

配置

 output: {
        path: path.resolve(__dirname, 'dist'),
        filename: "[name].js",
        chunkFilename: "[name].chunk.js"
}

解释配置

chunkFilename: "[name].chunk.js" 当你遇到动态引入的模块的时候,这个chunkFilename就会起作用!

如何动态引入?

两种动态引入的方式,一种自己写的,一种react自带的。

  1. 自己写的
    const getAsyncComponent =(load)=>{
        return class AsyncComponent extends React.Component{
            componentDidMount() {
                load().then(({default: Component})=>{
                    this.setState({
                        Component
                    })
                })
            }
            render() {
                const {Component} = this.state || {}
                return Component ? <Component {...this.props}/> : <div>loading...</div>
            }
        }
    }
    
    const asyncUser = getAsyncComponent(()=>import(/* webpackChunkName:'page-user'*/'@pages/user'))
    
  2. react自带的 Suspense lazy
    lazy(()=>import(/* webpackChunkName:'page-user'*/'@pages/user'))
    
  3. 全部代码
    import React, { Suspense, Component, lazy } from 'react'
    import  ReactDom from 'react-dom'
    import './index.scss'
    import { Route, BrowserRouter, Switch } from 'react-router-dom'
    import Home from '@pages/home';
    
    // import {User} from "@pages/user";
    // import {About} from "@pages/about";
    // 如果不注销这个同步的import,那么chunk就不能动态生成...
    
    // const asyncUserComponent = ()=>import(/* webpackChunkName: 'page-user' */'@pages/user').then(({default: component})=> component())
    
    const getAsyncComponent =(load)=>{
        return class AsyncComponent extends React.Component{
            componentDidMount() {
                load().then(({default: Component})=>{
                    this.setState({
                        Component
                    })
                })
            }
            render() {
                const {Component} = this.state || {}
                return Component ? <Component {...this.props}/> : <div>loading...</div>
            }
        }
    }
    
    const asyncUser = getAsyncComponent(()=>import(/* webpackChunkName:'page-user'*/'@pages/user'))
    const asyncAbout = getAsyncComponent(()=>import(/* webpackChunkName:'page-about'*/'@pages/about'))
    
    class App extends React.Component{
        render(){
            return (
                <Suspense fallback={<div>loading...</div>}>
                    <BrowserRouter>
                        <Switch>
                                <Route exact path='/' component={Home}/>
                                <Route path='/user' component={lazy(()=>import(/* webpackChunkName:'page-user'*/'@pages/user'))}/>
                                <Route path='/about' component={asyncAbout}/>
                        </Switch>
                    </BrowserRouter>
                </Suspense>
    
            )
        }
    }
    
    ReactDom.render(<App/>,document.getElementById('root'))
    
    
  4. 解释
    ()=>import(/* webpackChunkName:'page-user'*/'@pages/user')
    
    webpackChunkName就是chunk的名称,最后这个chunk会生成一个文件,文件名叫做page-user.chunk.js

静态chunk

静态chunk就是传统import方式引入的chunk

比如:import React from 'react'

安装

配置

  optimization: {
        usedExports: true,
        splitChunks: {
            chunks: "all",
            cacheGroups: {
                vendors:{
                    test:/node_modules/,
                    priority:-10,
                },
                ui:{
                    test:/src\/components/,
                    minSize:0,
                    reuseExistingChunk: true,
                    priority:-20
                }
            }
        }
    }

解释配置

如果这个import的模块是属于node_modules目录下的,就塞到vendors模块下,打包出来的文件名就叫做:vendors~main.chunk.js

如果这个import的模块是属于src/components目录下的,就塞到ui模块下,打包出来的文件名就叫做:ui~main.chunk.js

压缩js

这个功能一般是用到生产模式下的

安装

yarn add terser-webpack-plugin

配置

const TerserJSPlugin = require("terser-webpack-plugin");

 optimization:{
        minimizer: [
            new TerserJSPlugin({})
        ]
    },

解释配置

压缩js代码

css分离文件

安装

yarn add mini-css-extract-plugin

配置

const MiniCssExtractPlugin = require("mini-css-extract-plugin");

   module: {
        rules: [
            {
                test:/\.scss$/,
                loaders:[MiniCssExtractPlugin.loader,'css-loader','sass-loader']
            },
            {
                test:/\.css$/,
                loaders:[MiniCssExtractPlugin.loader,'css-loader']
            },
        ]
    }
    
    plugins: [
        new MiniCssExtractPlugin({
            filename: "[name].css",
            chunkFilename: "[id].css"
        })
    ],

解释配置

把css代码从main.js文件里面剥离出一个单独的css文件。

压缩css

一般用在生产模式下。

安装

yarn add optimize-css-assets-webpack-plugin

配置

  optimization:{
        minimizer: [
            new OptimizeCSSAssetsPlugin({})
        ]
    }

解释配置

css代码被压缩了

DllPlugin

我们只希望第三方的模块在第一次打包的时候分析,以后都不分析了。

加快打包的速度!

安装

yarm add add-asset-html-webpack-plugin

配置

  1. 创建webpack.dll.js
const {DllPlugin} = require('webpack')
const path = require('path')

module.exports={
    mode:'production',
    entry:{
        react:['react','react-dom'],
        time:['timeago.js']
    },
    output:{
        filename: "[name].dll.js",
        path: path.resolve(__dirname, 'dll'),
        library: '[name]'
    },
    plugins:[
        new DllPlugin({
            name:'[name]',
            path: path.resolve(__dirname, './dll/[name].manifest.json')
        })
    ]
}

  1. "dll": "webpack --config webpack.dll.js"

  2. 配置webpack.config.js

const fs = require('fs')
const {DllReferencePlugin} = require('webpack')
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin')

const dllPlugins = ()=>{
    const plugins = []
    const files = fs.readdirSync(path.resolve(__dirname, './dll'))
    files.forEach(file => {
        if (/.*\.dll.js/.test(file)){
            plugins.push(new AddAssetHtmlPlugin({
                filepath: path.resolve(__dirname, './dll',file)
            }))
        }
        if (/.*\.manifest.json/.test(file)){
            plugins.push(new DllReferencePlugin({
                manifest:path.resolve(__dirname, './dll', file)
            }))
        }
    })
    return plugins;
}

 plugins: [
    ...dllPlugins()
 ]

解释配置

1.注意

先运行yarn run dll,这样先解析webpack.dll.js,生成了dll文件夹以及关于dll的文件。

再执行yarn start,这样在运行webpack.config.js的时候,执行到fs.readdirSync(path.resolve(__dirname, './dll'))这一步才不会因为找不到文件夹文件而出错!

2.DllReferencePlugin :

意思是当我们打包的时候,我们发现第三方的模块,我们之前会从node_modules里面一遍一遍的找

现在我们会先从dll/vendors.manifest.json里面找映射关系

如果第三方模块在映射关系里,我们知道,这个第三方模块,就在vendors.dll.js里面,

那么就会从全局变量里面拿, 因为第三方模块第一次打包的时候,就生成里全局变量了

就不用再在node_modules里面一点一点分析,一点一点找了,加快了打包的速度

3.AddAssetHtmlPlugin:

最后把我么打包生成的*.dll.js文件,作为静态文件插入到我们的index.html里面

分离环境

开发环境跟到生产环境是不一样的,有些东西开发环境能用,生产环境不见得能用,比如devServer但是有些代码又是公用的,比如css-loader

开发环境跟到生产环境注重的东西也不一样。开发环境更注重写代码的效率,方便。生产环境更注重包的大小,轻便。

所以,要针对不同的环境做不同的配置!

安装

yarn add webpack-merge

配置

比如生产环境:(开发环境类似啦)

const merge = require('webpack-merge')
const baseConfig = require('./webpack.base')

const prodConfig = {...}

module.exports=merge(baseConfig, prodConfig)

解释配置

在不同的环境,根据不同的侧重,做不同的事!

最后

继续学习