• 基于webpack4搭建一个react脚手架


    react-sample-javascript

    React 16.0 boilerplate with react-router-dom, redux & webpack 4. (for javascript)


    项目初始化

    统一规范代码格式

    1. 配置 .editorconfig 使得IDE的方式统一 (见代码)
    2. 配置 .eslintrc.js 使得代码规范统一 (见代码)

    预期功能

    1. 管理资源: 能加载css、sccc、less、以及静态文件
    2. 管理输出:将打包后的静态文件输出至static目录下,以各自的文件类型管理
    3. dev:使用source map,方便调试时代码定位
    4. dev:配置devServer,并配置热替换,热加载,自动刷新,自动打开浏览器,并预留proxyTable
    5. dev:设置默认打开8080,被占用则寻找下一个空接口
    6. production:代码分离,打包css文件,css代码压缩,js代码压缩,输出到模板html,配置gzip
    7. analysis::使用BundleAnalyzerPlugin 分析打包后的性能

    目录结构

    • 首先使用npm init 初始化一个包含package.json的根目录
    :.
    │  .babelrc      		#babel的规则以及插件
    │  .editorconfig		#IDE/编辑器相关的配置
    │  .eslintignore		#Eslint忽视的目录
    │  .eslintrc.js			#Eslint的规则和插件
    │  .gitignore			#Git忽视的目录
    │  .postcssrc.js		#postcss的插件
    │  package-lock.json
    │  package.json			#项目相关的包
    │  README.md
    │  yarn.lock
    │
    ├─build					#webpack相关的配置
    │      utils.js			#webpack配置中的通用方法
    │      webpack.base.conf.js	#webpack的基础配置
    │      webpack.dev.conf.js	#webpack的开发环境配置
    │      webpack.prod.conf.js	#webpack的生产环境配置
    │
    └─src					#主目录,业务代码
        │  app.css
        │  App.js
        │  favicon.ico
        │  index.ejs
        │  index.js
        │
        └─assets			#静态目录,存放静态资源
            │  config.json
            │
            └─img
                    logo.svg
    

    安装依赖

    • eslint-loader
    • eslint
    • eslint-config-airbnb
    • eslint-plugin-import
    • eslint-friendly-formatter
    • eslint-plugin-flowtype
    • eslint-plugin-jsx-a11y
    • eslint-plugin-react
    • babel-polyfill
    • webpack
    • jest
    • friendly-errors-webpack-plugin 编译提示的webpack插件
    • html-webpack-plugin 新建html入口文件的webpack插件
    • copy-webpack-plugin webpack配置合并模块
    • webpack-merge webpack配置合并模块
    • webpack-dev-server
    • webpack-bundle-analyzer
    • webpack-cli
    • portfinder 寻找接口的插件
    • extract-text-webpack-plugin
    • node-notifier
    • optimize-css-assets-webpack-plugin
    • autoprefixer
    • mini-css-extract-plugin
    • autoprefixer
    • css-loader
    • less-loader
    • postcss-loader
    • postcss-import
    • postcss-loader
    • style-loader
    • babel-core
    • babel-eslint
    • babel-loader
    • babel-plugin-transform-runtime
    • babel-plugin-import
    • babel-preset-env
    • babel-preset-react
    • babel-polyfill
    • url-loader
    • cross-env
    • file-loader
    yarn add eslint eslint-loader eslint-config-airbnb eslint-plugin-import eslint-friendly-formatter eslint-plugin-flowtype eslint-plugin-jsx-a11y eslint-plugin-react babel-polyfill webpack jest webpack-merge copy-webpack-plugin html-webpack-plugin friendly-errors-webpack-plugin webpack-dev-server webpack-bundle-analyzer webpack-cli portfinder extract-text-webpack-plugin node-notifier optimize-css-assets-webpack-plugin autoprefixer mini-css-extract-plugin autoprefixer css-loader less-loader postcss-loader postcss-import postcss-loader style-loader babel-core babel-eslint babel-loader babel-plugin-transform-runtime babel-plugin-import babel-preset-env babel-preset-react babel-polyfill url-loader cross-env file-loader -D
    
    

    项目配置

    webpack 基础配置

    1. 为了控制开发环境和生产环境,我们可以新建build文件夹。分别书写开发环境和生产环境的webpack配置文件,这样也更可以方便我们分别控制生产环境和开发环境。
    2. 为了提高代码的复用率,也为了区别 基础配置个性配置 ,可以分别新建webpack.basewebpack.devwebpack.prod三个配置文件。首先配置最基础的entry(入口)和output(出口)。
    module.exports = {
      context: path.resolve(__dirname, '../'),	//绝对路径。__dirname为当前目录。
        //基础目录用于从配置中解析入口起点。因为webpack配置在build下,所以传入 '../'
      entry: {
        app: ('./src/index.js') //项目的入口
      },
      output: {
        path: path.resolve(__dirname, '../dist'),
        filename: '[name].[hash:8].js',
        publicPath: '/',
        libraryTarget: 'umd',
      },
    }
    

    entry

    entry可以分别为字符串、数组和对象。

    倘若应用只有一个单一的入口,entry的值可以使用任意类型,不会影响输出结果。

    // entry为字符串
    {
        entry: './src/index.js',
        output: {
        	path: '/dist',
            filename: 'bundle.js'
        }
    }
    // 结果会生成 '/dist/bundle.js'
    
    // entry为数组,可以添加多个彼此不互相依赖的文件。结合output.library选项,如果传入数组,则只导出最后一项。
    {
        //如果你在html文件里引入了'bable-polyfill',可以通过数组将它加到bundle.js的最后。
        entry: ['./src/index.js', 'babel-polyfill'] ,
        output:{
            path: '/dist',
            filename: 'bundle.js'
        }
    }
    
    // entry为对象,可以将页面配置为多页面的而不是SPA,有多个html文件。通过对象告诉webpack为每个入口,成一个bundle文件。
    // 多页面的配置,可能还要借助于HtmlWebpackPlugin,来指定每个html需要引入的js
    {
        entry: {
            index: './src/index.js'
            main: './src/index.js'
            login: './src/login.js'
        }
        output:{
            path: '/dist/pages'
            filename: '[name]-[hash:5].js' //文件名取自'entry'对象的键名,为了防止推送代码后浏览器读缓存,故再生成的文件之后加上hash码。
        }
    }
    // 会分别生成index.js,main.js,login.js三个文件
    

    关于 webpack构建多页面 可以参考这篇文章。不过现在webpack4.x也是一次断崖式升级,感兴趣的同学可以自行搜索。

    // entry也可以传入混合类型
    {
        entry:{
            vendor: ['jquery','amap','babel-polyfill'] //也可以借助CommonsChunkPlugin提取vendor的chunk。
            index: './src/index.js'
        }
        output: {
            path: '/dist'
            filename: '[name]-[hash:5].js'
        }
    }
    

    CommonsChunkPlugin在webpack4.0之后移除了,可以使用splitChunksPlugin代替。

    可以参阅如下链接:optimization.splitChunks


    output

    output最基础的两个配置为 pathfilename

    • path 告诉 webpack的输出目录在那里,一般我们会设置在根目录的 dist 文件夹;
    • filename 用于指定输出文件的文件名,如果配置了创建了多个单独的 chunk 则可以使用[name].[hash] 这种占位符来确保每个文件有唯一的名称;
    • 另一个常见配置 publicPath 则是用于更加复杂的场景。举例:在本地时,你可能会使用 ../assets/test.png 这种url来载入图片。而在生产环境下,你可能会使用CDN或者图床的地址。那么就需要配置 publicPath = "http://cdn.example.com/assets/" 来实现生产模式下编译输出文件时自动更新url。
     output: {
        path: path.resolve(__dirname, '../dist'),
        filename: '[name].[hash:8].js',
        publicPath: '/',
      },
    

    resolve

    resolve常用的两个配置为 aliasextensions

    • alias 创建import或者require的别名
    • extensins 自动解析文件拓展名,补全文件后缀
    resolve: {
        // 自动解析文件扩展名(补全文件后缀)(从左->右)
        // import hello from './hello'  (!hello.js? -> !hello.jsx? -> !hello.json)
        extensions: ['.js', '.jsx', '.json'],
        alias: {
          '@': resolve('src')
        }
      },
    

    module

    module的选项决定了如何处理项目中的不同类型的模块。其中常用的有 rulesnoParese 两个配置项。

    • noParese 是为了防止weback解析与所有与rule相匹配的文件。目的是,忽略大型的library可以提高构建性能。
    noParse: function(content) {
      return /jquery|lodash/.test(content);
    }
    
    • rules 用于在创建模块是,匹配规则数组,以确定哪些规则能够对module应用loader,或者是修改parser。
    module: {
        rules: [
        {
            test: /.(js|jsx)$/,
            exclude: /node_modules/,
            enforce: 'pre',
            use: [{
              loader: 'babel-loader',
            }, {
              loader: 'eslint-loader', // 指定启用eslint-loader
              options: {
                formatter: require('eslint-friendly-formatter'),
                emitWarning: false
              }
            }]
          },
        {
            test: /.css$/,
            include: /node_modules/,
            use: [
              MiniCssExtractPlugin.loader,
              'css-loader',
              {
                loader: 'postcss-loader',
                options: {
                  plugins: () => [autoprefixer({ browsers: 'last 5 versions' })],
                  sourceMap: false,
                },
              },
            ],
          },
          {
            test: /.(png|jpe?g|gif|svg)(?.*)?$/,
            loader: 'url-loader',
            options: {
              limit: 10000,
              name: ('assets/img/[name].[hash:7].[ext]')
            }
          },
          {
            test: /.(mp4|webm|ogg|mp3|wav|flac|aac)(?.*)?$/,
            loader: 'url-loader',
            options: {
              limit: 10000,
              name: ('assets/media/[name].[hash:7].[ext]')
            }
          },
          {
            test: /.(woff2?|eot|ttf|otf)(?.*)?$/,
            loader: 'url-loader',
            options: {
              limit: 10000,
              name: ('assets/fonts/[name].[hash:7].[ext]')
            }
          }
        ]
        }
    

    例如上述代码,就使用eslint-lodaerbabel-loader 处理了除了node_modules 以外的 js||jsx。同时配置了,解析图片、视频、字体文件等的解析,当rules匹配到的文件时,小于10000 byte 时,采用url-loader解析文件。(因为base64会让图片的体积变大,所以当文件较大时,使用base64并不明智)

    Webpack开发配置

    因为在webpack 4.X 中使用了流行的 ”约定大于配置“ 的做法,所以在新加入配置项 mode ,可以告知webpack使用相应模式的内置优化。

    选项 描述
    development 会将process.env.NODE_ENV 的值设为 development 。启用 NamedChunksPluginNamedMoudulesPlugin
    production 会将process.env.NODE_ENV 的值设为 production 。启用 FlagDependencyUsagePluginFlagIncludedChunksPluginModuleConcatenationPluginNoEmitOnErrorsPluginOccurrenceOrderPluginSideEffectsFlagPlugin UglifyJsPlugin

    如果我们只设置NODE_ENV,则不会自动设置 mode

    在开发时,我们往往希望能看到当前开发的页面,并且能热加载。这时,我们可以借助webpack-dev-server 这个插件,来在项目中起一个应用服务器。

    // package.json
    "scripts": {
        "start": "webpack-dev-server --mode development --config build/webpack.dev.conf.js",
    }
    // 设置当前的mode为development,同样这个配置也可以写在webpack.dev.conf.js中。然后使用build目录下的webpack.dev.conf.js 来配置相关的webpack。
    
    devServer: {
        clientLogLevel: 'warning',
        historyApiFallback: true, //在开发单页应用时非常有用,它依赖于HTML5 history API,如果设置为true,所有的跳转将指向index.html
        contentBase: path.resolve(__dirname, '../src'),
        compress: true,
        hot: true, // 热加载
        inline: true, //自动刷新
        open: true, //自动打开浏览器
        host: HOST||'localhost',
        port: PORT,
        overlay: { warnings: false, errors: true }, // 在浏览器上全屏显示编译的errors或warnings。
        publicPath: '/',
        proxy: {},
        quiet: true, // necessary for FriendlyErrorsPlugin // 终端输出的只有初始启动信息。 webpack 的警告和错误是不输出到终端的
        watchOptions: {
          poll: false
        }
      },
      plugins: [
        new webpack.DefinePlugin({
          ...process.env
        }),
        //开启HMR(热替换功能,替换更新部分,不重载页面!)
        new webpack.HotModuleReplacementPlugin(),// HMR shows correct file names in console on update.
        //显示模块相对路径
        new webpack.NamedModulesPlugin(),
        //不显示错误信息
        new webpack.NoEmitOnErrorsPlugin(),
        // https://github.com/ampedandwired/html-webpack-plugin
        ]
    

    其实在开发时,我们可以设置 contentBase: '/src'contentBase 指定了devServer能访问的资源地址。因为我们开发时,资源大部分都放在src目录下,所以可以直接指定资源路径为src目录。因为我们在webpack基础配置时,配置了 output 输出为 dist 目录,所以我们也可以在devServer里,设置 contentBasedist 目录。不过此时需要使用copyWebpackPlugin将一些静态资源复制到 dist 目录下,手动新建dist目录,并复制也可以。

    另外,当使用 history 路由时,要配置 historyApiFallback = true ,以此让服务器放弃路由权限,交由前端路由。而是用 hash 路由则不需要此配置。

    项目进阶

    生产环境配置

    在使用webpack 4.x 的 mode 配置之后,需要我们手动配置的项已经减少了很多,像js代码压缩这种工具 UglifyJsPlugin 就已经不用手动去配置。但是像很多前面提到的 代码分离css代码提取和压缩html的生成 以及 复制静态资源 还需要我们手动配置。

    代码分离

    // 设置代码分离的输出目录
    output: {
        path: path.resolve(__dirname, '../dist'),
        filename: ('js/[name].[hash:8].js'),
        chunkFilename: ('js/[name]-[id].[hash:8].js')
      },
     // 代码分离
     optimization: {
        runtimeChunk: {
          name: "manifest"
        },
        splitChunks: {
          chunks: 'all'
        }
      },
    

    可以参阅如下链接:optimization.splitChunks


    css代码压缩

    借助 MiniCssExtractPlugin 来实现压缩css和提取css。因为 MiniCssExtractPlugin 无法与style-loader 共存,所以我们需要判断当前环境是生成环境还是开发环境。

    我们可以新建一个util.js的文件,在webpack当中一些共用的方法。考虑使用个别配置字段 extract 来配置使用何种方式来配置css-loader。参见 util.js 代码。

    new MiniCssExtractPlugin({
          filename: 'css/[name].[hash:8].css',
          chunkFilename: 'css/[name]-[id].[hash:8].css',
        }),
    

    生成HTML

    使用htmlWebpackPlugin,配合ejs。可以使控制html 的生成。通过配置的方式,生成html。因为 HtmlWebpackPlugin 本身可以解析ejs,所以不需要单独引入ejs的loader。

    new HtmlWebpackPlugin({
          filename: 'index.html',
          template: './src/index.ejs', // 设置目录
          title: 'React Demo',
          inject: true, // true->'head' || false->'body'
          minify: {
            //删除Html注释
            removeComments: true,
            //去除空格
            collapseWhitespace: true,
            //去除属性引号
            removeAttributeQuotes: true
            // more options:
            // https://github.com/kangax/html-minifier#options-quick-reference
          },
          // necessary to consistently work with multiple chunks via CommonsChunkPlugin
          chunksSortMode: 'dependency'
        }),
    
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <meta name="robots" content="noindex, nofollow">
    
      <title><%= htmlWebpackPlugin.options.title %></title>
      <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
      <link rel="icon" href="/favicon.ico" type="image/x-icon">
    
      <% for (var chunk in htmlWebpackPlugin.files.css) { %>
      <link rel="preload" href="<%= htmlWebpackPlugin.files.css[chunk] %>"  as="style">
      <% } %>
      <% for (var chunk in htmlWebpackPlugin.files.chunks) { %>
      <link rel="preload" href="<%= htmlWebpackPlugin.files.chunks[chunk].entry %>" as="script">
      <% } %>
    
      <base href="/">
    </head>
    <body>
    <div id="root"></div>
    </body>
    <style type="text/css">
      body {
        font-family: 'Source Sans Pro','Helvetica Neue',Helvetica,Arial,sans-serif;
      }
    </style>
    </html>
    
    

    复制静态目录

    将所以可能被请求的静态文件,分别放在assets目录下。那么在打包后,为了保证目录能正常访问(不使用CDN等加载静态资源时),我们可以配置 publicPath = '/' 。然后借助于 CopyWebpackPlugin 实现资源复制。

    new CopyWebpackPlugin([{
          from: './src/assets/',
          to: 'assets'
        }]),
    

    src/assets 复制到 dist/assets 目录下。


    开启打包分析

    借助插件 BundleAnalyzerPlugin 直接在plugins中创建该插件:

    // webpack.prod.conf.js
    const BundleAnalyzerPlugin = process.env.NODE_ENV=== 'analysis' ? require('webpack-bundle-analyzer').BundleAnalyzerPlugin:null
    process.env.NODE_ENV=== 'analysis' ? new BundleAnalyzerPlugin() : ()=>{}
    

    在package.json 中可做如下配置:

    "scripts": {
        "analysis": "cross-env NODE_ENV=analysis webpack -p --mode production --progress --config ./build/webpack.prod.conf.js ",
      },
    

    通过注入环境变量,来控制是否运行打包分析。


    ssh部署

    打包后的dist文件夹,可以直接借助 node 的 ssh-node ,直接部署到服务器指定的目录下。 ssh-node既支持ssh,也支持密码登录。建议可以为在每个项目下,新建一个.ssh文件,存放项目的私钥。代码如下:

    // usage: https://www.npmjs.com/package/node-ssh
    var path, node_ssh, ssh, fs, opn, host
    
    fs = require('fs')
    path = require('path')
    node_ssh = require('node-ssh')
    opn = new require('opn')
    ssh = new node_ssh()
    host = 'localhost'
    var localDir = './dist'
    var remoteDir = '/opt/frontend/new'
    var removeCommand = 'rm -rf ./*'
    var pwdCommand = 'pwd'
    
    ssh.connect({
      host: host,
      username: 'root',
      port: 22,
      // password,
      privateKey: "./.ssh/id_rsa",
    })
      .then(function() {
        ssh.execCommand(removeCommand, { cwd:remoteDir }).then(function(result) {
          console.log('STDOUT: ' + result.stdout)
          console.log('STDERR: ' + result.stderr)
          ssh.putDirectory(localDir, remoteDir).then(function() {
            console.log("The File thing is done")
            ssh.dispose()
            opn('http://'+host, {app:['chrome']})
          }, function(error) {
            console.log("Something's wrong")
            console.log(error)
            ssh.dispose()
          })
        })
      })
    
    

    此时,在命令行直接 node deploy.js 就可以运行以上脚本,我们也可以添加一个build + deploy的script脚本,便于启动。

    "scripts": {
        "depoly": "npm run build && node ./deploy.js",
    }
    

    结语

    本次从零到一,新建了一个react脚手架。过程中有很多问题,也参考了不少大牛的解释。代码里也有诸多问题。还望各位看官,不吝指教。
    记得留下你的足迹哦。

    参考

    参考了vue-cli v2.96的webpack配置。
    【翻译】Webpack——令人困惑的地方
    webpack中文文档

  • 相关阅读:
    为TextBox定义快捷键
    (转)再谈“我是怎么招聘程序员的”
    (转)jQuery框架学习第二天:jQuery中万能的选择器
    (转)MongoDB学习笔记(一) MongoDB介绍及安装
    asp.net 导出CSV
    领域驱动设计下系统层次结构风格(转载)
    (转)谈谈数据加密的处理提供各种算法处理
    (转)REST服务开发实战
    领域驱动设计系列文章(2)——浅析VO、DTO、DO、PO的概念、区别和用处
    (转)你的工作不是命令人们去做什么
  • 原文地址:https://www.cnblogs.com/DDante/p/9368694.html
Copyright © 2020-2023  润新知