从零开始手写一个优化版React脚手架

webpack立刻要出5了,彻底手写一个优化后的脚手架是不可或缺的技能。css

  • 本文书写时间 2019年5月9日 , webpack版本 4.30.0最新版本html

  • 要转载必须联系本人通过赞成才可转载 谢谢!node

  • 杜绝5分钟的技术,咱们先深刻原理再写配置,那会简单不少。react

  • 实现需求:webpack

  • 识别JSX文件git

  • tree shaking 摇树优化 删除掉无用代码es6

  • PWA功能,热刷新,安装后当即接管浏览器 离线后仍让能够访问网站 还能够在手机上添加网站到桌面使用github

  • CSS模块化,不怕命名冲突web

  • 小图片的base64处理面试

  • 文件后缀省掉jsx js json

  • 实现懒加载,按需加载 , 代码分割

  • 支持less sass stylus等预处理

  • code spliting 优化首屏加载时间 不让一个文件体积过大

  • 提取公共代码,打包成一个chunk

  • 每一个chunk有对应的chunkhash,每一个文件有对应的contenthash,方便浏览器区别缓存

  • 图片压缩

  • CSS压缩

  • 增长CSS前缀 兼容各类浏览器

  • 对于各类不一样文件打包输出指定文件夹下

  • 缓存babel的编译结果,加快编译速度

  • 每一个入口文件,对应一个chunk,打包出来后对应一个文件 也是code spliting

  • 删除HTML文件的注释等无用内容

  • 每次编译删除旧的打包代码

  • CSS文件单独抽取出来

  • 等等....

  • webpack中文官网的标语是 :让一切都变得简单

  • 概念:
    • 本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序须要的每一个模块,而后将全部这些模块打包成一个或多个 bundle

    • webpack v4.0.0开始,能够不用引入一个配置文件。然而,webpack 仍然仍是高度可配置的。在开始前你须要先理解四个核心概念:

      • 入口(entry)
      • 输出(output)
      • loader
      • 插件(plugins) 本文旨在给出这些概念的高度概述,同时提供具体概念的详尽相关用例。

让咱们一块儿来复习一下最基础的Webpack知识,若是你是高手,那么请直接忽略这些往下看吧....

  • 入口

    • 入口起点`(entry point)指示 webpack 应该使用哪一个模块,来做为构建其内部依赖图的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。

    • 每一个依赖项随即被处理,最后输出到称之为 bundles的文件中,咱们将在下一章节详细讨论这个过程。

    • 能够经过在 webpack配置中配置entry 属性,来指定一个入口起点(或多个入口起点)。默认值为 ./src

    • 接下来咱们看一个 entry 配置的最简单例子:

    webpack.config.js
    
    module.exports = {
      entry: './path/to/my/entry/file.js'
    };
    复制代码
    • 入口能够是一个对象,也能够是一个纯数组
    entry: {
        app: ['./src/index.js', './src/index.html'],
        vendor: ['react'] 
    },
    entry: ['./src/index.js', './src/index.html'],
    复制代码
    • 有人可能会说,入口怎么放HTML文件,由于开发模式下热更新若是不设置入口为HTML,那么更改了HTML文件内容,是不会刷新页面的,须要手动刷新,因此这里给了入口HTML文件,一个细节。
  • 出口(output)

    • output 属性告诉 webpack 在哪里输出它所建立的 bundles,以及如何命名这些文件,默认值为 ./dist。基本上,整个应用程序结构,都会被编译到你指定的输出路径的文件夹中。你能够经过在配置中指定一个 output 字段,来配置这些处理过程:
webpack.config.js
    
    const path = require('path');
    
    module.exports = {
      entry: './path/to/my/entry/file.js',
      output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'my-first-webpack.bundle.js'
      }
    };
复制代码

在上面的示例中,咱们经过 output.filenameoutput.path 属性,来告诉 webpack bundle 的名称,以及咱们想要 bundle 生成(emit)到哪里。可能你想要了解在代码最上面导入的 path 模块是什么,它是一个Node.js 核心模块,用于操做文件路径。

  • loader
    • loader 让 webpack 可以去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 能够将全部类型的文件转换为 webpack 可以处理的有效模块,而后你就能够利用 webpack 的打包能力,对它们进行处理。

    • 本质上,webpack loader 将全部类型的文件,转换为应用程序的依赖图(和最终的 bundle)能够直接引用的模块。

    • 注意,loader 可以 import 导入任何类型的模块(例如 .css 文件),这是 webpack 特有的功能,其余打包程序或任务执行器的可能并不支持。咱们认为这种语言扩展是有很必要的,由于这可使开发人员建立出更准确的依赖关系图。

    • 在更高层面,在 webpack 的配置中 loader 有两个目标:

    • test 属性,用于标识出应该被对应的 loader 进行转换的某个或某些文件。

    • use 属性,表示进行转换时,应该使用哪一个 loader。

    webpack.config.js
        
        const path = require('path');
        
        const config = {
          output: {
            filename: 'my-first-webpack.bundle.js'
          },
          module: {
            rules: [
              { test: /\.txt$/, use: 'raw-loader' }
            ]
          }
        };
        
        module.exports = config;
    复制代码
    • 以上配置中,对一个单独的module对象定义了 rules 属性,里面包含两个必须属性:test 和 use。这告诉 webpack 编译器(compiler) 以下信息:

    • “嘿,webpack编译器,当你碰到「在require()/import 语句中被解析为 '.txt' 的路径」时,在你对它打包以前,先使用 raw-loader转换一下。”

    • 重要的是要记得,在 webpack 配置中定义loader 时,要定义在module.rules 中,而不是 rules。然而,在定义错误时webpack会给出严重的警告。为了使你受益于此,若是没有按照正确方式去作,webpack 会“给出严重的警告”

    • loader 还有更多咱们还没有提到的具体配置属性。

    • 这里引用这位做者的优质文章内容,手写一个loaderplugin [手写一个loader和plugin][2]

高潮来了 ,webpack的编译原理 ,为何要先学学习原理? 由于你起码得知道你写的是干什么的!

  • webpack打包原理

    • 识别入口文件

    • 经过逐层识别模块依赖。(Commonjs、amd或者es6的import,webpack都会对其进行分析。来获取代码的依赖)

    • webpack作的就是分析代码。转换代码,编译代码,输出代码

    • 最终造成打包后的代码

    • 这些都是webpack的一些基础知识,对于理解webpack的工做机制颇有帮助。

  • 什么是loader

    • loader是文件加载器,可以加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一块儿打包到指定的文件中

    • 处理一个文件可使用多个loaderloader的执行顺序是和自己的顺序是相反的,即最后一个loader最早执行,第一个loader最后执行。

    • 第一个执行的loader接收源文件内容做为参数,其余loader接收前一个执行的loader的返回值做为参数。最后执行的loader会返回此模块的JavaScript源码

    • 在使用多个loader处理文件时,若是要修改outputPath输出目录,那么请在最上面的loader中options设置

  • 什么是plugin?

    • Webpack 运行的生命周期中会广播出许多事件,Plugin 能够监听这些事件,在合适的时机经过 Webpack 提供的 API 改变输出结果。

    • plugin和loader的区别是什么?

    • 对于loader,它就是一个转换器,将A文件进行编译造成B文件,这里操做的是文件,好比将A.scss或A.less转变为B.css,单纯的文件转换过程

    • plugin是一个扩展器,它丰富了wepack自己,针对是loader结束后,webpack打包的整个过程,它并不直接操做文件,而是基于事件机制工做,会监听webpack打包过程当中的某些节点,执行普遍的任务。

  • webpack的运行

    • webpack 启动后,在读取配置的过程当中会先执行 new MyPlugin(options)初始化一个 MyPlugin 得到其实例。在初始化compiler 对象后,再调用myPlugin.apply(compiler) 给插件实例传入compiler 对象。插件实例在获取到 compiler 对象后,就能够经过 compiler.plugin(事件名称, 回调函数) 监听到 Webpack广播出来的事件。而且能够经过compiler对象去操做webpack
    • 看到这里可能会问compiler是啥,compilation又是啥?
    • Compiler 对象包含了 Webpack 环境全部的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局惟一的,能够简单地把它理解为 Webpack 实例;
    • Compilation对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被建立。Compilation对象也提供了不少事件回调供插件作扩展。经过 Compilation 也能读取到Compiler 对象。
    • CompilerCompilation 的区别在于:
    • Compiler 表明了整个Webpack从启动到关闭的生命周期,而Compilation只是表明了一次新的编译。
  • 事件流

    • webpack经过Tapable来组织这条复杂的生产线。
    • webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。
    • webpack的事件流机制应用了观察者模式,和Node.js 中的 EventEmitter很是类似。

下面正式开始开发环境的配置:

  • 入口设置 :
    • 设置APP,几个入口文件,即会最终分割成几个chunk
    • 在入口中配置 vendor,能够code spliting,将这些公共的复用代码最终抽取成一个chunk,单独打包出来
    • 要想在开发模式中HMTL文件也热更新,须要加入·index.html为入口文件
entry: {
            app: ['./src/index.js', './src/index.html'],
            vendor: ['react']  //这里还能够加入redux react-redux better-scroll等公共代码 
        },
复制代码
  • output出口
    • webpack基于Node.js环境运行,可使用Node.jsAPIpath模块的resolve方法
    • 对输出的JS文件,加入contenthash标示,让浏览器缓存文件,区别版本。
output: {
            filename: '[name].[contenthash:8].js',
            path: resolve(__dirname, '../dist')
        },
复制代码
  • mode: 'development' 模式选择,这里直接设置成开发模式,先从开发模式开始。
  • resolve解析配置,为了为了给全部文件后缀省掉 js jsx json,加入配置
    resolve: {
        extensions: [".js", ".json", ".jsx"]
    }
    复制代码
  • 加入插件 热更新pluginhtml-webpack-plugin
const HtmlWebpackPlugin = require('html-webpack-plugin')
   const webpack = require('webpack')
   new HtmlWebpackPlugin({
           template: './src/index.html'
       }),
   new webpack.HotModuleReplacementPlugin(),
复制代码
  • 加入 babel-loader 还有 解析JSX ES6语法的 babel preset
    • @babel/preset-react解析 jsx语法
    • @babel/preset-env解析es6语法
    • @babel/plugin-syntax-dynamic-import解析react-loadableimport按需加载,附带code spliting功能
{
        test: /\.(js|jsx)$/,
        use:
        {
            loader: 'babel-loader',
            options: {
                presets: ["@babel/preset-react", ["@babel/preset-env", { "modules": false }]],
                plugins: ["@babel/plugin-syntax-dynamic-import"]
                     },

        }
   },
  
复制代码
  • React的按需加载,附带代码分割功能 ,每一个按需加载的组件打包后都会被单独分割成一个文件
import React from 'react'
        import loadable from 'react-loadable'
        import Loading from '../loading' 
        const LoadableComponent = loadable({
            loader: () => import('../Test/index.jsx'),
            loading: Loading,
        });
        class Assets extends React.Component {
            render() {
                return (
                    <div>
                        <div>这即将按需加载</div>
                        <LoadableComponent />
                    </div>
                )
            }
        }
        
        export default Assets

复制代码
  • 加入html-loader识别html文件
{
    test: /\.(html)$/,
    loader: 'html-loader'
    }
            ```
* 加入`eslint-loader` 
复制代码
{
    enforce:'pre',
    test:/\.js$/,
    exclude:/node_modules/,
    include:resolve(__dirname,'/src/js'),
    loader:'eslint-loader'
    }
            ```
复制代码
  • 开发模式结束 代码在下面
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpack = require('webpack')
const WorkboxPlugin = require('workbox-webpack-plugin')
module.exports = {
    entry: {
        app: ['./src/index.js', './src/index.html'],
        vendor: ['react', ]
    },
    output: {
        filename: '[name].[hash:8].js',
        path: resolve(__dirname, '../build')
    },
    module: {
        rules: [
            {
                enforce:'pre',
                test:/\.js$/,
                exclude:/node_modules/,
                include:resolve(__dirname,'/src/js'),
                loader:'eslint-loader'
                },
            {
                oneOf: [{
                    test: /\.(html)$/,
                    loader: 'html-loader'
                },
            {
                test: /\.(js|jsx)$/,
                use:
                {
                    loader: 'babel-loader',
                    options: {
                        presets: ["@babel/preset-react", ["@babel/preset-env", { "modules": false }]],
                        plugins: ["@babel/plugin-syntax-dynamic-import"]
                    },

                }
            },
            {
                test: /\.(less)$/,
                use: [
                    { loader: 'style-loader' },
                    {
                        loader: 'css-loader', options: {
                            modules: true,
                            localIdentName: '[local]--[hash:base64:5]'
                        }
                    },
                    { loader: 'less-loader' }
                ]
            }, {
                test: /\.(jpg|jpeg|bmp|svg|png|webp|gif)$/,
                loader: 'url-loader',
                options: {
                    limit: 8 * 1024,
                    name: '[name].[hash:8].[ext]',
                    
                }
            }, {
                exclude: /\.(js|json|less|css|jsx)$/,
                loader: 'file-loader',
                options: {
                    outputPath: 'media/',
                    name: '[name].[hash].[ext]'
                }
            }
            ]
        }]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './src/index.html'
        }),
        new webpack.HotModuleReplacementPlugin(),
        new webpack.NamedModulesPlugin(), //打包时候能够看到文件名的插件
    ],
    mode: 'development',
    devServer: {
        contentBase: '../build',
        open: true,
        port: 3000,
        hot: true
    },
    resolve: {
        extensions: [".js", ".json", ".jsx"]
    }
}
复制代码

必须了解的webpack热更新原理 :

  • webpack的热更新又称热替换(Hot Module Replacement),缩写为HMR。 这个机制能够作到不用刷新浏览器而将新变动的模块替换掉旧的模块。

  • 首先要知道server端和client端都作了处理工做

    • 第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块从新编译打包,并将打包后的代码经过简单的JavaScript对象保存在内存中。

    • 第二步是webpack-dev-serverwebpack 之间的接口交互,而在这一步,主要是 dev-server的中间件 webpack-dev-middleware 和 webpack之间的交互,webpack-dev-middleware调用webpack 暴露的 API对代码变化进行监控,而且告诉webpack,将代码打包到内存中。

    • 第三步是 webpack-dev-server 对文件变化的一个监控,这一步不一样于第一步,并非监控代码变化从新打包。当咱们在配置文件中配置了devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念。

    • 第四步也是webpack-dev-server 代码的工做,该步骤主要是经过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间创建一个 websocket 长链接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不一样的操做。固然服务端传递的最主要信息仍是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。

    • webpack-dev-server/client 端并不可以请求更新的代码,也不会执行热更模块操做,而把这些工做又交回给了 webpack,webpack/hot/dev-server 的工做就是根据webpack-dev-server/client传给它的信息以及dev-server 的配置决定是刷新浏览器呢仍是进行模块热更新。固然若是仅仅是刷新浏览器,也就没有后面那些步骤了。

    • HotModuleReplacement.runtime是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash值,它经过 JsonpMainTemplate.runtime向 server 端发送 Ajax 请求,服务端返回一个json,该 json 包含了全部要更新的模块的 hash 值,获取到更新列表后,该模块再次经过 jsonp 请求,获取到最新的模块代码。这就是上图中 七、八、9 步骤。

    • 而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。

    • 最后一步,当 HMR失败后,回退到 live reload 操做,也就是进行浏览器刷新来获取最新打包代码。

    • 参考文章 [webpack面试题-腾讯云][4]

正式开始生产环节:

  • 加入 WorkboxPluginPWA的插件
    • pwa这个技术其实要想真正用好,仍是须要下点功夫,它有它的生命周期,以及它在浏览器中热更新带来的反作用等,须要认真研究。能够参考百度的lavas框架发展历史~
const WorkboxPlugin = require('workbox-webpack-plugin')


    new WorkboxPlugin.GenerateSW({ 
                clientsClaim: true, //让浏览器当即servece worker被接管
                skipWaiting: true,  // 更新sw文件后,当即插队到最前面 
                importWorkboxFrom: 'local',
                include: [/\.js$/, /\.css$/, /\.html$/,/\.jpg/,/\.jpeg/,/\.svg/,/\.webp/,/\.png/],
            }),
        
复制代码
  • 加入每次打包输出文件清空上次打包文件的插件
const CleanWebpackPlugin = require('clean-webpack-plugin')
    
    new CleanWebpackPlugin()
复制代码
  • 加入code spliting代码分割
optimization: {
            runtimeChunk:true,  //设置为 true, 一个chunk打包后就是一个文件,一个chunk对应`一些js css 图片`等
            splitChunks: {
                chunks: 'all'  // 默认 entry 的 chunk 不会被拆分, 配置成 all, 就能够了拆分了,一个入口`JS`,
                //打包后就生成一个单独的文件
            }
        }
复制代码
  • 加入图片压缩
{
                test: /\.(jpg|jpeg|bmp|svg|png|webp|gif)$/,
                
                use:[
                    {loader: 'url-loader',
                    options: {
                        limit: 8 * 1024,
                        name: '[name].[hash:8].[ext]',
                        outputPath:'/img'
                    }},
                    {
                        loader: 'img-loader',
                        options: {
                          plugins: [
                            require('imagemin-gifsicle')({
                              interlaced: false
                            }),
                            require('imagemin-mozjpeg')({
                              progressive: true,
                              arithmetic: false
                            }),
                            require('imagemin-pngquant')({
                              floyd: 0.5,
                              speed: 2
                            }),
                            require('imagemin-svgo')({
                              plugins: [
                                { removeTitle: true },
                                { convertPathData: false }
                              ]
                            })
                          ]
                        }
                      }
                ]
                
                

            }
            ```
 


* 加入单独抽取`CSS`文件的`loader`和插件
复制代码

const MiniCssExtractPlugin = require('mini-css-extract-plugin')

{
    test: /\.(less)$/,
    use: [
        MiniCssExtractPlugin.loader,
        {
            loader: 'css-loader', options: {
                modules: true,
                localIdentName: '[local]--[hash:base64:5]'
            }
        },
        {loader:'postcss-loader'},
        { loader: 'less-loader' }
    ]
}

 new MiniCssExtractPlugin({
        filename:'[name].[contenthash:8].css'
    }),
复制代码
* 加入压缩`css`的插件
复制代码
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
new OptimizeCssAssetsWebpackPlugin({
            cssProcessPluginOptions:{
                preset:['default',{discardComments: {removeAll:true} }]
            }
        }),
复制代码
* 杀掉`html`一些没用的代码
复制代码
new HtmlWebpackPlugin({
    template: './src/index.html',
    minify: {
        removeComments: true,  
        collapseWhitespace: true,  
        removeRedundantAttributes: true,
        useShortDoctype: true, 
        removeEmptyAttributes: true,
        removeStyleLinkTypeAttributes: true,
        keepClosingSlash: true, 
        minifyJS: true,
        minifyCSS: true, 
        minifyURLs: true, 
     }
复制代码

}),

* 加入`file-loader` 把一些文件打包输出到固定的目录下



               exclude: /\.(js|json|less|css|jsx)$/,
               loader: 'file-loader',
               options: {
                   outputPath: 'media/',
                   name: '[name].[contenthash:8].[ext]'
               }
           }
           
           
           





> 里面有一些注释可能不详细,代码都是本身一点点写,试过的,确定没用任何问题

* 须要的依赖
* 

{
   "name": "webpack",
   "version": "1.0.0",
   "main": "index.js",
   "license": "MIT",
   "dependencies": {
       "@babel/core": "^7.4.4",
       "@babel/preset-env": "^7.4.4",
       "@babel/preset-react": "^7.0.0",
       "autoprefixer": "^9.5.1",
       "babel-loader": "^8.0.5",
       "clean-webpack-plugin": "^2.0.2",
       "css-loader": "^2.1.1",
       "eslint": "^5.16.0",
       "eslint-loader": "^2.1.2",
       "file-loader": "^3.0.1",
       "html-loader": "^0.5.5",
       "html-webpack-plugin": "^3.2.0",
       "imagemin": "^6.1.0",
       "imagemin-gifsicle": "^6.0.1",
       "imagemin-mozjpeg": "^8.0.0",
       "imagemin-pngquant": "^7.0.0",
       "imagemin-svgo": "^7.0.0",
       "img-loader": "^3.0.1",
       "less": "^3.9.0",
       "less-loader": "^5.0.0",
       "mini-css-extract-plugin": "^0.6.0",
       "optimize-css-assets-webpack-plugin": "^5.0.1",
       "postcss-loader": "^3.0.0",
       "react": "^16.8.6",
       "react-dom": "^16.8.6",
       "react-loadable": "^5.5.0",
       "react-redux": "^7.0.3",
       "style-loader": "^0.23.1",
       "url-loader": "^1.1.2",
       "webpack": "^4.30.0",
       "webpack-cli": "^3.3.2",
       "webpack-dev-server": "^3.3.1",
       "workbox-webpack-plugin": "^4.3.1"
   },
   "scripts": {
       "start": "webpack-dev-server --config ./config/webpack.dev.js",
       "dev": "webpack-dev-server --config ./config/webpack.dev.js",
       "build": "webpack --config ./config/webpack.prod.js "
   },
   "devDependencies": {
       "@babel/plugin-syntax-dynamic-import": "^7.2.0"
   }
}




# 整个项目和`webpack`配置的源码地址 : [源码地址啊 看得见吗亲][5]

# **路过的小伙伴麻烦点个赞给个star,写得好辛苦啊!!!!**


 [1]: /img/bVbspk3
 [2]: /img/bVbspEk
 [3]: /img/bVbspE4
 [4]: webpackjs.com/configuration/module/#rule-oneof
 [5]: https://github.com/JinJieTan/React-webpack复制代码
相关文章
相关标签/搜索