中级-框架基础(下)

68 阅读19分钟

十、webpack 和 babel

webpack面试题--解析

1. 前端代码为何要进行构建和打包

代码层面
体积更小(Tree-Shaking、压缩、合并)
编译高级语言或语法(TS、ES6+、模块化、scss)
兼容性和错误检查(Polyfill、postcss、eslint)
前端工程化、前端流程、团队效率层面
统一、高效的开发环境
统一的构建流程和产出标准
集成公司构建规范(提测、上线等)

2. module chunk bundle分别什么意思,有何区别?

3.babel-runtime和babel-polyfill的区别

babel-polyfill会污染全局
babel-runtime不会污染全局
产出第三方lib要用babel-runtime

4. babel和webpack的区别

babel - JS新语法编译工具,不关心模块化\
webpack - 打包构建工具,是多个loader plugin的集合

5. 如何产出一个lib

参考webpack.dll.js\
output.library

image.png

6. 为何Proxy不能被polyfill

Class可以用function模拟\
如Promise可以用callback来模拟\
但Proxy的功能用Object.defineProperty无法模拟

1.于webpack类似的工具还有哪些?谈谈你为什么选择使用或者放弃webpack?

2.如何调试webpack代码

3. loader和plugin的区别

4.webpack的构建流程是什么

5.有哪些常见的loader和plugin?他们是解决什么问题的?

6.source map是什么?生产环境怎么用?

7.如何利用webpack来优化前端性能?(或者说:webpack常见性能优化)

8.webpack中hash、chunkhash、contenthash区别

9.如何对bundle体积进行监控和分析

10.如何提高webpack的构建速度

11.inline loade、pre loader、post loader和normal loader执行的先后顺序是什么?

12.是否写过loader?描述一下编写loader的思路

13.是否写过plugin?描述一下编写plugin的思路

14.webpack打包的原理是什么?聊一聊babel和抽象语法树

15.tree-shaking了解么,它的实现原理说一下

16.webpack的热更新是如何做到的?说明其原理

17.从零实现Webpack5模块联邦原理并实现微前端

基本配置

拆分配置和merge

const webpackCommonConf = require('./webpack.common.js')
const { smart } = require('webpack-merge')

module.exports = smart(webpackCommonConf, {
})

启动本地服务

    devServer: {
        port: 8080,
        progress: true,  // 显示打包的进度条
        contentBase: distPath,  // 根目录
        open: true,  // 自动打开浏览器
        compress: true,  // 启动 gzip 压缩

        // 设置代理
        proxy: {
            // 将本地 /api/xxx 代理到 localhost:3000/api/xxx
            '/api': 'http://localhost:3000',

            // 将本地 /api2/xxx 代理到 localhost:3000/xxx
            '/api2': {
                target: 'http://localhost:3000',
                pathRewrite: {
                    '/api2': ''
                }
            }
        }
    }

处理ES6

{
      test: /\.js$/,
      loader: ['babel-loader'],
      include: srcPath,
      exclude: /node_modules/
},
//.babelrc
{
    "presets": ["@babel/preset-env"],
    "plugins": []
}

处理样式

  // {
  //     test: /\.css$/,
  //     // loader 的执行顺序是:从后往前(知识点)
  //     loader: ['style-loader', 'css-loader']
  // },
  {
      test: /\.css$/,
      // loader 的执行顺序是:从后往前
      loader: ['style-loader', 'css-loader', 'postcss-loader'] // 加了 postcss
  },
  {
      test: /\.less$/,
      // 增加 'less-loader' ,注意顺序
      loader: ['style-loader', 'css-loader', 'less-loader']
  }
// postcss-loader是为了处理兼容性,还要配置postcss.config.js才会生效
//postcss.config.js
module.exports = {
    plugins: [require('autoprefixer')]
}

处理图片

 // dev直接引入图片 url
 {
     test: /\.(png|jpg|jpeg|gif)$/,
     use: 'file-loader'
 }
 
 //prod情况小于 5kb 的图片用 base64 格式产出,其他产出 url 格式放在img目录下
 // 图片 - 考虑 base64 编码的情况
 {
     test: /\.(png|jpg|jpeg|gif)$/,
     use: {
         loader: 'url-loader',
         options: {
             // 小于 5kb 的图片用 base64 格式产出
             // 否则,依然延用 file-loader 的形式,产出 url 格式
             limit: 5 * 1024,

             // 打包到 img 目录下
             outputPath: '/img1/',

             // 设置图片的 cdn 地址(也可以统一在外面的 output 中设置,那将作用于所有静态资源)
             // publicPath: 'http://cdn.abc.com'
         }
     }
 },

模块化

总结

//package.json
{
  "name": "07-webpack-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "devBuild": "webpack --config build-optimization/webpack.dev.js",
    "dev": "webpack-dev-server --config build-optimization/webpack.dev.js",
    "build": "webpack --config build-optimization/webpack.prod.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.7.4",
    "@babel/preset-env": "^7.7.4",
    "autoprefixer": "^9.7.3",
    "babel-loader": "^8.0.6",
    "clean-webpack-plugin": "^3.0.0",
    "css-loader": "^3.2.1",
    "file-loader": "^5.0.2",
    "happypack": "^5.0.1",
    "html-webpack-plugin": "^3.2.0",
    "less": "^3.10.3",
    "less-loader": "^5.0.0",
    "mini-css-extract-plugin": "^0.8.0",
    "optimize-css-assets-webpack-plugin": "^5.0.3",
    "postcss-loader": "^3.0.0",
    "style-loader": "^1.0.1",
    "terser-webpack-plugin": "^2.2.2",
    "url-loader": "^3.0.0",
    "webpack": "^4.41.2",
    "webpack-cli": "^3.3.10",
    "webpack-dev-server": "^3.9.0",
    "webpack-merge": "^4.2.2",
    "webpack-parallel-uglify-plugin": "^1.1.2"
  },
  "dependencies": {
    "lodash": "^4.17.15",
    "moment": "^2.24.0"
  }
}
//paths.js
/**
 * @description 常用文件夹路径
 * @author 双越
 */

const path = require('path')

const srcPath = path.join(__dirname, '..', 'src')
const distPath = path.join(__dirname, '..', 'dist')

module.exports = {
    srcPath,
    distPath
}
//webpack.common.js 
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { srcPath, distPath } = require('./paths')

module.exports = {
    entry: path.join(srcPath, 'index'),
    module: {
        rules: [
            {
                test: /\.js$/,
                loader: ['babel-loader'],
                include: srcPath,
                exclude: /node_modules/
            },
            // {
            //     test: /\.vue$/,
            //     loader: ['vue-loader'],
            //     include: srcPath
            // },
            // {
            //     test: /\.css$/,
            //     // loader 的执行顺序是:从后往前(知识点)
            //     loader: ['style-loader', 'css-loader']
            // },
            {
                test: /\.css$/,
                // loader 的执行顺序是:从后往前
                loader: ['style-loader', 'css-loader', 'postcss-loader'] // 加了 postcss
            },
            {
                test: /\.less$/,
                // 增加 'less-loader' ,注意顺序
                loader: ['style-loader', 'css-loader', 'less-loader']
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: path.join(srcPath, 'index.html'),
            filename: 'index.html'
        })
    ]
}
//webpack.dev.js
const path = require('path')
const webpack = require('webpack')
const webpackCommonConf = require('./webpack.common.js')
const { smart } = require('webpack-merge')
const { srcPath, distPath } = require('./paths')

module.exports = smart(webpackCommonConf, {
    mode: 'development',
    module: {
        rules: [
            // 直接引入图片 url
            {
                test: /\.(png|jpg|jpeg|gif)$/,
                use: 'file-loader'
            }
        ]
    },
    plugins: [
        new webpack.DefinePlugin({
            // window.ENV = 'development'
            ENV: JSON.stringify('development')
        })
    ],
    devServer: {
        port: 8080,
        progress: true,  // 显示打包的进度条
        contentBase: distPath,  // 根目录
        open: true,  // 自动打开浏览器
        compress: true,  // 启动 gzip 压缩

        // 设置代理
        proxy: {
            // 将本地 /api/xxx 代理到 localhost:3000/api/xxx
            '/api': 'http://localhost:3000',

            // 将本地 /api2/xxx 代理到 localhost:3000/xxx
            '/api2': {
                target: 'http://localhost:3000',
                pathRewrite: {
                    '/api2': ''
                }
            }
        }
    }
})
//webpack.prod.js
const path = require('path')
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const webpackCommonConf = require('./webpack.common.js')
const { smart } = require('webpack-merge')
const { srcPath, distPath } = require('./paths')

module.exports = smart(webpackCommonConf, {
    mode: 'production',
    output: {
        filename: 'bundle.[contentHash:8].js',  // 打包代码时,加上 hash 戳
        path: distPath,
        // publicPath: 'http://cdn.abc.com'  // 修改所有静态文件 url 的前缀(如 cdn 域名),这里暂时用不到
    },
    module: {
        rules: [
            // 图片 - 考虑 base64 编码的情况
            {
                test: /\.(png|jpg|jpeg|gif)$/,
                use: {
                    loader: 'url-loader',
                    options: {
                        // 小于 5kb 的图片用 base64 格式产出
                        // 否则,依然延用 file-loader 的形式,产出 url 格式
                        limit: 5 * 1024,

                        // 打包到 img 目录下
                        outputPath: '/img1/',

                        // 设置图片的 cdn 地址(也可以统一在外面的 output 中设置,那将作用于所有静态资源)
                        // publicPath: 'http://cdn.abc.com'
                    }
                }
            },
        ]
    },
    plugins: [
        new CleanWebpackPlugin(), // 会默认清空 output.path 文件夹
        new webpack.DefinePlugin({
            // window.ENV = 'production'
            ENV: JSON.stringify('production')
        })
    ]
})
//.babelrc
{
    "presets": ["@babel/preset-env"],
    "plugins": []
}
//postcss.config.js
module.exports = {
    plugins: [require('autoprefixer')]
}

高级配置

基本配置只能做demo,不能做线上项目
面试考察基本配置,只是为了快速判断你是否用过webpack
以下高级配置,也是通过面试的必要条件

多入口

//webpack.common.js
    entry: {
        index: path.join(srcPath, 'index.js'),
        other: path.join(srcPath, 'other.js')
    },
       // 多入口 - 生成 index.html
   new HtmlWebpackPlugin({
       template: path.join(srcPath, 'index.html'),
       filename: 'index.html',
       // chunks 表示该页面要引用哪些 chunk (即上面的 index 和 other),默认全部引用
       chunks: ['index']  // 只引用 index.js
   }),
   // 多入口 - 生成 other.html
   new HtmlWebpackPlugin({
       template: path.join(srcPath, 'other.html'),
       filename: 'other.html',
       chunks: ['other']  // 只引用 other.js
   })
//webpack.prod.js
output: {
        // filename: 'bundle.[contentHash:8].js',  // 打包代码时,加上 hash 戳
        filename: '[name].[contentHash:8].js', // name 即多入口时 entry 的 key
        path: distPath,
        // publicPath: 'http://cdn.abc.com'  // 修改所有静态文件 url 的前缀(如 cdn 域名),这里暂时用不到
    },

抽离css文件

//webpack.prod.js
const path = require('path')
const webpack = require('webpack')
const { smart } = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const TerserJSPlugin = require('terser-webpack-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const webpackCommonConf = require('./webpack.common.js')
const { srcPath, distPath } = require('./paths')

module.exports = smart(webpackCommonConf, {
    mode: 'production',
    output: {
        // filename: 'bundle.[contentHash:8].js',  // 打包代码时,加上 hash 戳
        filename: '[name].[contentHash:8].js', // name 即多入口时 entry 的 key
        path: distPath,
        // publicPath: 'http://cdn.abc.com'  // 修改所有静态文件 url 的前缀(如 cdn 域名),这里暂时用不到
    },
    module: {
        rules: [
            // 图片 - 考虑 base64 编码的情况
            {
                test: /\.(png|jpg|jpeg|gif)$/,
                use: {
                    loader: 'url-loader',
                    options: {
                        // 小于 5kb 的图片用 base64 格式产出
                        // 否则,依然延用 file-loader 的形式,产出 url 格式
                        limit: 5 * 1024,

                        // 打包到 img 目录下
                        outputPath: '/img1/',

                        // 设置图片的 cdn 地址(也可以统一在外面的 output 中设置,那将作用于所有静态资源)
                        // publicPath: 'http://cdn.abc.com'
                    }
                }
            },
            // 抽离 css
            {
                test: /\.css$/,
                loader: [
                    MiniCssExtractPlugin.loader,  // 注意,这里不再用 style-loader
                    'css-loader',
                    'postcss-loader'
                ]
            },
            // 抽离 less --> css
            {
                test: /\.less$/,
                loader: [
                    MiniCssExtractPlugin.loader,  // 注意,这里不再用 style-loader
                    'css-loader',
                    'less-loader',
                    'postcss-loader'
                ]
            }
        ]
    },
    plugins: [
        new CleanWebpackPlugin(), // 会默认清空 output.path 文件夹
        new webpack.DefinePlugin({
            // window.ENV = 'production'
            ENV: JSON.stringify('production')
        }),

        // 抽离 css 文件
        new MiniCssExtractPlugin({
            filename: 'css/main.[contentHash:8].css'
        })
    ],

    optimization: {
        // 压缩 css
        minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
    }
})


//webpack.dev.js
const path = require('path')
const webpack = require('webpack')
const webpackCommonConf = require('./webpack.common.js')
const { smart } = require('webpack-merge')
const { srcPath, distPath } = require('./paths')

module.exports = smart(webpackCommonConf, {
    mode: 'development',
    module: {
        rules: [
            // 直接引入图片 url
            {
                test: /\.(png|jpg|jpeg|gif)$/,
                use: 'file-loader'
            },
            // {
            //     test: /\.css$/,
            //     // loader 的执行顺序是:从后往前
            //     loader: ['style-loader', 'css-loader']
            // },
            {
                test: /\.css$/,
                // loader 的执行顺序是:从后往前
                loader: ['style-loader', 'css-loader', 'postcss-loader'] // 加了 postcss
            },
            {
                test: /\.less$/,
                // 增加 'less-loader' ,注意顺序
                loader: ['style-loader', 'css-loader', 'less-loader']
            }
        ]
    },
    plugins: [
        new webpack.DefinePlugin({
            // window.ENV = 'production'
            ENV: JSON.stringify('development')
        })
    ],
    devServer: {
        port: 8080,
        progress: true,  // 显示打包的进度条
        contentBase: distPath,  // 根目录
        open: true,  // 自动打开浏览器
        compress: true,  // 启动 gzip 压缩

        // 设置代理
        proxy: {
            // 将本地 /api/xxx 代理到 localhost:3000/api/xxx
            '/api': 'http://localhost:3000',

            // 将本地 /api2/xxx 代理到 localhost:3000/xxx
            '/api2': {
                target: 'http://localhost:3000',
                pathRewrite: {
                    '/api2': ''
                }
            }
        }
    }
})
//webpack.common.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { srcPath, distPath } = require('./paths')

module.exports = {
    entry: {
        index: path.join(srcPath, 'index.js'),
        other: path.join(srcPath, 'other.js')
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                loader: ['babel-loader'],
                include: srcPath,
                exclude: /node_modules/
            }
            // css 处理
        ]
    },
    plugins: [
        // new HtmlWebpackPlugin({
        //     template: path.join(srcPath, 'index.html'),
        //     filename: 'index.html'
        // })

        // 多入口 - 生成 index.html
        new HtmlWebpackPlugin({
            template: path.join(srcPath, 'index.html'),
            filename: 'index.html',
            // chunks 表示该页面要引用哪些 chunk (即上面的 index 和 other),默认全部引用
            chunks: ['index']  // 只引用 index.js
        }),
        // 多入口 - 生成 other.html
        new HtmlWebpackPlugin({
            template: path.join(srcPath, 'other.html'),
            filename: 'other.html',
            chunks: ['other']  // 只引用 other.js
        })
    ]
}

抽离公共代码

//webpack.prod.js
const path = require('path')
const webpack = require('webpack')
const { smart } = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const TerserJSPlugin = require('terser-webpack-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const webpackCommonConf = require('./webpack.common.js')
const { srcPath, distPath } = require('./paths')

module.exports = smart(webpackCommonConf, {
    mode: 'production',
    output: {
        // filename: 'bundle.[contentHash:8].js',  // 打包代码时,加上 hash 戳
        filename: '[name].[contentHash:8].js', // name 即多入口时 entry 的 key
        path: distPath,
        // publicPath: 'http://cdn.abc.com'  // 修改所有静态文件 url 的前缀(如 cdn 域名),这里暂时用不到
    },
    module: {
        rules: [
            // 图片 - 考虑 base64 编码的情况
            {
                test: /\.(png|jpg|jpeg|gif)$/,
                use: {
                    loader: 'url-loader',
                    options: {
                        // 小于 5kb 的图片用 base64 格式产出
                        // 否则,依然延用 file-loader 的形式,产出 url 格式
                        limit: 5 * 1024,

                        // 打包到 img 目录下
                        outputPath: '/img1/',

                        // 设置图片的 cdn 地址(也可以统一在外面的 output 中设置,那将作用于所有静态资源)
                        // publicPath: 'http://cdn.abc.com'
                    }
                }
            },
            // 抽离 css
            {
                test: /\.css$/,
                loader: [
                    MiniCssExtractPlugin.loader,  // 注意,这里不再用 style-loader
                    'css-loader',
                    'postcss-loader'
                ]
            },
            // 抽离 less
            {
                test: /\.less$/,
                loader: [
                    MiniCssExtractPlugin.loader,  // 注意,这里不再用 style-loader
                    'css-loader',
                    'less-loader',
                    'postcss-loader'
                ]
            }
        ]
    },
    plugins: [
        new CleanWebpackPlugin(), // 会默认清空 output.path 文件夹
        new webpack.DefinePlugin({
            // window.ENV = 'production'
            ENV: JSON.stringify('production')
        }),

        // 抽离 css 文件
        new MiniCssExtractPlugin({
            filename: 'css/main.[contentHash:8].css'
        })
    ],

    optimization: {
        // 压缩 css
        minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],

        // 分割代码块
        splitChunks: {
            chunks: 'all',
            /**
             * initial 入口 chunk,对于异步导入的文件不处理
                async 异步 chunk,只对异步导入的文件处理
                all 全部 chunk
             */

            // 缓存分组
            cacheGroups: {
                // 第三方模块
                vendor: {
                    name: 'vendor', // chunk 名称
                    priority: 1, // 权限更高,优先抽离,重要!!!
                    test: /node_modules/,
                    minSize: 0,  // 大小限制,多少kb才打包,为了防止比较小的包打包出来反而浪费资源
                    minChunks: 1  // 最少复用过几次 引用了一次就单独抽离出
                },

                // 公共的模块
                common: {
                    name: 'common', // chunk 名称
                    priority: 0, // 优先级
                    minSize: 0,  // 公共模块的大小限制,多少kb才打包,为了防止比较小的包打包出来反而浪费资源
                    minChunks: 2  // 公共模块最少复用过几次 引用了2次就单独抽离出
                }
            }
        }
    }
})
//webpack.common.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { srcPath, distPath } = require('./paths')

module.exports = {
    entry: {
        index: path.join(srcPath, 'index.js'),
        other: path.join(srcPath, 'other.js')
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                loader: ['babel-loader'],
                include: srcPath,
                exclude: /node_modules/
            }
        ]
    },
    plugins: [
        // new HtmlWebpackPlugin({
        //     template: path.join(srcPath, 'index.html'),
        //     filename: 'index.html'
        // })

        // 多入口 - 生成 index.html
        new HtmlWebpackPlugin({
            template: path.join(srcPath, 'index.html'),
            filename: 'index.html',
            // chunks 表示该页面要引用哪些 chunk (即上面的 index 和 other),默认全部引用
            chunks: ['index', 'vendor', 'common']  // 要考虑代码分割
        }),
        // 多入口 - 生成 other.html
        new HtmlWebpackPlugin({
            template: path.join(srcPath, 'other.html'),
            filename: 'other.html',
            chunks: ['other', 'common']  // 考虑代码分割
        })
    ]
}

懒加载

//引入动态数据 - 懒加载
setTimeout(() => {
	import('./dynamic-data.js').then(res=>{
		console.log(res.default.message) //注意这里的default
	})
},1500)

处理JSX

//.babelrc 
{
    "presets": ["@babel/preset-react"],
    "plugins": []
}
//webpack配置
{
      test: /\.js$/,
      loader: ['babel-loader'],
      include: srcPath,
      exclude: /node_modules/
  },

处理vue

 {
     test: /\.vue$/,
     loader: ['vue-loader'],
     include: srcPath
 },

module chunk bundle的区别

module - 各个源码文件,webpack中一切皆模块,所有引入都是模块,js、css、img都是模块
chunk - 多模块合并成的,如entry import() splitChunk
bundle - 最终的输出文件,一般来说一个chunk对应一个bundle

image.png module chunk bundle分别对应图中的左中右

webpack性能优化

大厂必考 & 社区热议话题
优化打包构建速度 - 开发体验和效率
优化产出代码 - 产品性能

1)构建速度

优化babel-loader

image.png

开启缓存,es6代码没改的不会重新编译

IgnorePlugin避免引入无用的模块

import moment from ‘moment’
默认会引入所有语言JS代码,代码过大
如何只引入中文?

//webpack.prod.js
  // 忽略 moment 下的 /locale 目录
 new webpack.IgnorePlugin(/\.\/locale/, /moment/),
 //index.js
 import moment from 'moment'
import 'moment/locale/zh-cn' //手动引入中文语言
moment.locale('zh-cn')//设置语言为中文
console.log('locale',moment.locale)
console.log('data',moment().format('ll')) //2020年xx月xx日

noParse避免重复打包

min,js基本上已经采用模块化处理过的

image.png

IgnorePlugin vs noParse

IgnorePlugin 直接不引入,代码中没有
noParse引入,但不打包

happyPack多进程打包

JS单线程,开启多进程打包
提高构建速度(特别是多核CPU)

const HappyPack = require('happypack')
 {
      test: /\.js$/,
      // 把对 .js 文件的处理转交给 id 为 babel 的 HappyPack 实例
      use: ['happypack/loader?id=babel'],
      include: srcPath,
      // exclude: /node_modules/
  },
   // happyPack 开启多进程打包
  new HappyPack({
      // 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
      id: 'babel',
      // 如何处理 .js 文件,用法和 Loader 配置中一样
      loaders: ['babel-loader?cacheDirectory']
  }),

ParallelUglifyPlugin多进程压缩JS

webpack内置Uglify工具压缩JS
JS单线程,开启多进程压缩更快
和happyPack同理

const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')

// 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
new ParallelUglifyPlugin({
    // 传递给 UglifyJS 的参数
    // (还是使用 UglifyJS 压缩,只不过帮助开启了多进程)
    uglifyJS: {
        output: {
            beautify: false, // 最紧凑的输出
            comments: false, // 删除所有的注释
        },
        compress: {
            // 删除所有的 `console` 语句,可以兼容ie浏览器
            drop_console: true,
            // 内嵌定义了但是只用到一次的变量
            collapse_vars: true,
            // 提取出出现多次但是没有定义成变量去引用的静态值
            reduce_vars: true,
        }
    }
})
关于开启多进程

项目较大,打包较慢,开启多进程能提高速度
项目较小,打包更快,开启多进程会降低速度(进程开销)
按需使用

自动刷新

   // watch: true, // 开启监听,默认为 false
    // watchOptions: {
    //     ignored: /node_modules/, // 忽略哪些
    //     // 监听到变化发生后会等300ms再去执行动作,防止文件更新太快导致重新编译频率太高
    //     // 默认为 300ms
    //     aggregateTimeout: 300,
    //     // 判断文件是否发生变化是通过不停的去询问系统指定文件有没有变化实现的
    //     // 默认每隔1000毫秒询问一次
    //     poll: 1000
    // }

image.png

//devServer会默认开启自动刷新
    devServer: {
        port: 8080,
        progress: true,  // 显示打包的进度条
        contentBase: distPath,  // 根目录
        open: true,  // 自动打开浏览器
        compress: true,  // 启动 gzip 压缩

        hot: true,

        // 设置代理
        proxy: {
            // 将本地 /api/xxx 代理到 localhost:3000/api/xxx
            '/api': 'http://localhost:3000',

            // 将本地 /api2/xxx 代理到 localhost:3000/xxx
            '/api2': {
                target: 'http://localhost:3000',
                pathRewrite: {
                    '/api2': ''
                }
            }
        }
    },

热更新

自动刷新:整个网页全部刷新,速度较慢
自动刷新:整个网页全部刷新,状态会丢失
热更新:新代码生效,网页不刷新,状态不丢失

//webpack.dev.js
const HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin');
    entry: {
        // index: path.join(srcPath, 'index.js'),
        index: [
            'webpack-dev-server/client?http://localhost:8080/',
            'webpack/hot/dev-server',
            path.join(srcPath, 'index.js')
        ]
    },
    plugins: [
        new HotModuleReplacementPlugin()
    ],
    devServer: {
        port: 8080,
        progress: true,  // 显示打包的进度条
        contentBase: distPath,  // 根目录
        open: true,  // 自动打开浏览器
        compress: true,  // 启动 gzip 压缩

        hot: true,

        // 设置代理
        proxy: {
            // 将本地 /api/xxx 代理到 localhost:3000/api/xxx
            '/api': 'http://localhost:3000',

            // 将本地 /api2/xxx 代理到 localhost:3000/xxx
            '/api2': {
                target: 'http://localhost:3000',
                pathRewrite: {
                    '/api2': ''
                }
            }
        }
    },
//index.js
// 引入 css
import './style/style1.css'
import './style/style2.less'

import { sum } from './math'

const sumRes = sum(10, 20)
console.log('sumRes', sumRes)

 // 增加,开启热更新之后的代码逻辑
 if (module.hot) {
     module.hot.accept(['./math'], () => {
         const sumRes = sum(10, 30)
         console.log('sumRes in hot', sumRes)
     })
 }

DllPlugin动态链接库插件

针对一些比较大的库,第三方插件,没有必要每次打包时都打包一遍,可以事先打包好以后作为dll.然后引用它

前端框架如vue React,体积大,构建慢
较稳定,不常升级版本
同一个版本只构建一次即可,不用每次都重新构建

webpack已内置DllPlugin支持
DllPlugin - 打包出dll文件
DllReferencePlugin - 使用dll文件

//package.json
{
  "name": "08-webpack-dll-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "webpack-dev-server --config build/webpack.dev.js",
    "dll": "webpack --config build/webpack.dll.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^16.12.0",
    "react-dom": "^16.12.0"
  },
  "devDependencies": {
    "@babel/core": "^7.7.5",
    "@babel/preset-env": "^7.7.5",
    "@babel/preset-react": "^7.7.4",
    "babel-loader": "^8.0.6",
    "html-webpack-plugin": "^3.2.0",
    "webpack": "^4.41.2",
    "webpack-cli": "^3.3.10",
    "webpack-dev-server": "^3.9.0",
    "webpack-merge": "^4.2.2"
  }
}
//webpack.dll.js
const path = require('path')
const DllPlugin = require('webpack/lib/DllPlugin')
const { srcPath, distPath } = require('./paths')

module.exports = {
  mode: 'development',
  // JS 执行入口文件
  entry: {
    // 把 React 相关模块的放到一个单独的动态链接库
    react: ['react', 'react-dom']
  },
  output: {
    // 输出的动态链接库的文件名称,[name] 代表当前动态链接库的名称,
    // 也就是 entry 中配置的 react 和 polyfill
    filename: '[name].dll.js',
    // 输出的文件都放到 dist 目录下
    path: distPath,
    // 存放动态链接库的全局变量名称,例如对应 react 来说就是 _dll_react
    // 之所以在前面加上 _dll_ 是为了防止全局变量冲突
    library: '_dll_[name]',
  },
  plugins: [
    // 接入 DllPlugin
    new DllPlugin({
      // 动态链接库的全局变量名称,需要和 output.library 中保持一致
      // 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值
      // 例如 react.manifest.json 中就有 "name": "_dll_react"
      name: '_dll_[name]',
      // 描述动态链接库的 manifest.json 文件输出时的文件名称
      path: path.join(distPath, '[name].manifest.json'),
    }),
  ],
}
npm run dll后dist文件夹下会生成react.dll.js和react.manifest.json
//index.html
 <script src="./react.dll.js"></script>
//webpack.dev.js

// 第一,引入 DllReferencePlugin
const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');

 module: {
        rules: [
            {
                test: /\.js$/,
                loader: ['babel-loader'],
                include: srcPath,
                exclude: /node_modules/ // 第二,不要再转换 node_modules 的代码
            },
        ]
    },
plugins: [
    // 第三,告诉 Webpack 使用了哪些动态链接库
    new DllReferencePlugin({
        // 描述 react 动态链接库的文件内容
        manifest: require(path.join(distPath, 'react.manifest.json')),
    }),
],

总结

webpack优化构建速度(可用于生产环境)

优化babel-loader
IgnorePlugin
noParse
happyPack
ParallelUglifyPlugin

webpack优化构建速度(不用于生产环境)

自动刷新
热更新
DllPlugin

2)优化产出代码

体积更小
合理分包,不重复加载
速度更快、内存使用更小

小图片base64编码

bundle加hash

懒加载

提取公共代码

IgnorePlugin使打出体积更小

使用CDN加速

//webpack.prod.js中配置cdn地址
 output: {
        publicPath: 'http://cdn.abc.com'  // 修改所有静态文件 url 的前缀(如 cdn 域名),这里暂时用不到
    },
  {
    test: /\.(png|jpg|jpeg|gif)$/,
    use: {
        loader: 'url-loader',
        options: {
            // 小于 5kb 的图片用 base64 格式产出
            // 否则,依然延用 file-loader 的形式,产出 url 格式
            limit: 5 * 1024,

            // 打包到 img 目录下
            outputPath: '/img1/',

            // 设置图片的 cdn 地址(也可以统一在外面的 output 中设置,那将作用于所有静态资源)
            publicPath: 'http://cdn.abc.com'
        }
    }
}
把静态文件上传至CDN
使用production
  • 自动开启代码压缩
  • Vue React等会自动删掉调试代码(如开发环境的warning)
  • 启动Tree-Shaking,没有用到的方法不会加载,把没有用的的摇掉
  • // ES6 Module 才能让 tree-shaking 生效
  • // commonjs 就不行
ES6 Module和commonjs的区别

ES6 Module静态引入,编译时引入
Commonjs动态引入,执行时引入
只有ES6 Module才能静态分析,实现Rree-Shaking

image.png

Scope Hosting

image.png

image.png

一个函数就有一个作用域,很多文件就会产生很多函数,很多作用域\

image.png

开启Scope Hosting之后,很多文件都放在一个函数里,作用域数量更少,内存占用更少,也不会频繁的去跨作用域调用,JS代码也会执行得快一些

代码体积更小
创建函数作用域更少
代码可读性更好

image.png

babel

前端开发环境必备工具
同webpack,需要了解基本的配置和使用
面试考察概率不高,但要求必会

环境搭建 & 基本配置

  • 环境搭建
  • .babelrc配置
  • presets和plugins
//package.json 
{
  "name": "09-babel-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.7.5",
    "@babel/core": "^7.7.5",
    "@babel/plugin-transform-runtime": "^7.7.5",
    "@babel/preset-env": "^7.7.5"
  },
  "dependencies": {
    "@babel/polyfill": "^7.7.0",
    "@babel/runtime": "^7.7.5"
  }
}
//.babelrc
{
    "presets": [
        [
            "@babel/preset-env",
            {
            }
        ]
    ],
    "plugins": [
    ]
}

preset-env是一堆常用plugins的集合

//index.js
const sum = (a,b) => a + b

npx babel src/index.js\

image.png

babel-polyfill

什么是Polyfill

浏览器补丁、兼容

core-js和regenerator

标准库,集成了ES6、ES7新语法的Polyfill

image.png

core-js不能处理generator函数,要用regenerator库

babel-polyfill即两者的集合

Babel7.4之后弃用babel-polyfill
推荐直接使用core-js和regenerator
但不影响面试会考察它

babel-polyfill如何按需引入

image.png babel只关心语法,即便API不支持,只要语法符合规范,不管API,也不管模块化
image.png

文件较大
只有一部分功能,无需全部引入
配置按需引入

//babelrc
{
    "presets": [
        [
            "@babel/preset-env",
            {
                "useBuiltIns": "usage",
                "corejs": 3
            }
        ]
    ],
    "plugins": [
    ]
}
//index.js
const sum = (a, b) => a + b

// 新的 API
Promise.resolve(100).then(data => data);

// 新的 API
[10, 20, 30].includes(20)

// 语法,符合 ES5 语法规范
// 不处理模块化(webpack)

image.png

没有使用babel-polyfill,直接引入core-js,babel-polyfill可以不用下载

babel-runtime

babel-polyfill的问题

  • 会污染全局环境
  • 如果做一个独立的web系统,则无碍
  • 如果做一个第三方lib,则会有问题
 // core.js要这样处理,会污染全局环境
// window.Promise1 = function() {}
// Array.prototype.includes1 = function () {}

// 使用方可能这样用,会出现问题
// window.Promise = 'abc'
// Array.prototype.includes = 100

babel-runtime处理这个问题

devDependencies安装 “@babel/plugin-transform-runtime”: “^7.7.5”,
dependencies安装 “@babel/runtime”: “^7.7.5”

//.babelrc 
{
    "presets": [
        [
            "@babel/preset-env",
            {
                "useBuiltIns": "usage",
                "corejs": 3
            }
        ]
    ],
    "plugins": [
        [
            "@babel/plugin-transform-runtime",
            {
                "absoluteRuntime": false,
                "corejs": 3,
                "helpers": true,
                "regenerator": true,
                "useESModules": false
            }
        ]
    ]
}

image.png 配置后编译出来的promise和includes前面有_,就不会有污染全局作用域问题

十一、项目设计

面试题

React设计todolist(组件结构,redux state 数据结构)
Vue设计购物车(组件结构,vuex state数据结构)

React实现Todo List

image.png

state数据结构设计

用数据描述所有的内容
数据要结构化,易于程序操作(遍历、查找)
数据要可扩展,以便增加新的功能 image.png

组件设计(拆分、组合)和组件通讯

从功能上拆分层次
尽量让组件原子化(一个组件只负责一个功能)
容器组件(只管理数据)& UI组件(只显示视图)

image.png

image.png

代码演示

//index.js
import React from 'react'
import List from './List'
import InputItem from './InputItem'

class App extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            list: [
                {
                    id: 1,
                    title: '标题1',
                    completed: false
                },
                {
                    id: 2,
                    title: '标题2',
                    completed: false
                },
                {
                    id: 3,
                    title: '标题3',
                    completed: false
                }
            ]
        }
    }
    render() {
        return <div>
            <InputItem addItem={this.addItem}/>
            <List
                list={this.state.list}
                deleteItem={this.deleteItem}
                toggleCompleted={this.toggleCompleted}
            />
        </div>
    }
    // 新增一项
    addItem = (title) => {
        const list = this.state.list
        this.setState({
            // 使用 concat 返回不可变值
            list: list.concat({
                id: Math.random().toString().slice(-5), // id 累加
                title,
                completed: false
            })
        })
    }
    // 删除一项
    deleteItem = (id) => {
        this.setState({
            // 使用 filter 返回不可变值
            list: this.state.list.filter(item => item.id !== id)
        })
    }
    // 切换完成状态
    toggleCompleted = (id) => {
        this.setState({
            // 使用 map 返回不可变值
            list: this.state.list.map(item => {
                const completed = item.id === id
                    ? !item.completed
                    : item.completed // 切换完成状态
                // 返回新对象
                return {
                    ...item,
                    completed
                }
            })
        })
    }
}

export default App
//List.js
import React from 'react'
import ListItem from './ListItem'

function List({ list = [], deleteItem, toggleCompleted }) {
    return <div>
        {list.map(item => <ListItem
            item={item}
            key={item.id}
            deleteItem={deleteItem}
            toggleCompleted={toggleCompleted}
        />)}
    </div>
}

export default List

//ListItem.js
import React from 'react'
import CheckBox from './UI/CheckBox'

class ListItem extends React.Component {
    render() {
        const { item } = this.props

        return <div style={{ marginTop: '10px' }}>
            <CheckBox onChange={this.completedChangeHandler}/>
            <span style={{ textDecoration: item.completed ? 'line-through' : 'none' }}>
                {item.title}
            </span>
            <button onClick={this.deleteHandler}>删除</button>
        </div>
    }
    completedChangeHandler = (checked) => {
        console.log('checked', checked)
        const { item, toggleCompleted } = this.props
        toggleCompleted(item.id)
    }
    deleteHandler = () => {
        const { item, deleteItem } = this.props
        deleteItem(item.id)
    }
}

export default ListItem

//CheckBox.js 
import React from 'react'

class CheckBox extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            checked: false
        }
    }
    render() {
        return <input type="checkbox" checked={this.state.checked} onChange={this.onCheckboxChange}/>
    }
    onCheckboxChange = () => {
        const newVal = !this.state.checked
        this.setState({
            checked: newVal
        })

        // 传给父组件
        this.props.onChange(newVal)
    }
}

export default CheckBox
//InputItem.js
import React from 'react'
import Input from './UI/Input'

class InputItem extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            title: ''
        }
    }
    render() {
        return <div>
            <Input value={this.state.title} onChange={this.changeHandler}/>
            <button onClick={this.clickHandler}>新增</button>
        </div>
    }
    changeHandler = (newTitle) => {
        this.setState({
            title: newTitle
        })
    }
    clickHandler = () => {
        const { addItem } = this.props
        addItem(this.state.title)

        this.setState({
            title: ''
        })
    }
}

export default InputItem
//Input.js
import React from 'react'

class Input extends React.Component {
    render() {
        return <input value={this.props.value} onChange={this.onChange}/>
    }
    onChange = (e) => {
        // 传给父组件
        const newVal = e.target.value
        this.props.onChange(newVal)
    }
}

export default Input

总结

state数据结构设计
组件设计组件通讯
结合redux

Vue实现购物车

image.png

data数据结构设计

用数据描述所有的内容
数据要结构化,易于程序操作(遍历、查找)
数据要可扩展,以便增加新的功能

image.png

组件设计和组件通讯

从功能上拆分层次
尽量让组件原子化
容器组件(只管理数据)& UI组件(只显示视图)

image.png

image.png

代码演示

//index.vue
<template>
    <div>
        <ProductionList :list="productionList"/>
        <hr>
        <CartList
            :productionList="productionList"
            :cartList="cartList"
        />
    </div>
</template>

<script>
import ProductionList from './ProductionList/index'
import CartList from './CartList/index'
import event from './event'

export default {
    components: {
        ProductionList,
        CartList
    },
    data() {
        return {
            productionList: [
                {
                    id: 1,
                    title: '商品A',
                    price: 10
                },
                {
                    id: 2,
                    title: '商品B',
                    price: 15
                },
                {
                    id: 3,
                    title: '商品C',
                    price: 20
                }
            ],
            cartList: [
                {
                    id: 1,
                    quantity: 1 // 购物数量
                }
            ]
        }
    },
    methods: {
        // 加入购物车
        addToCart(id) {
            // 先看购物车中是否有该商品
            const prd = this.cartList.find(item => item.id === id)
            if (prd) {
                // 数量加一
                prd.quantity++
                return
            }
            // 购物车没有该商品
            this.cartList.push({
                id,
                quantity: 1 // 默认购物数量 1
            })
        },
        // 从购物车删除一个(即购物数量减一)
        delFromCart(id) {
            // 从购物车中找出该商品
            const prd = this.cartList.find(item => item.id === id)
            if (prd == null) {
                return
            }

            // 数量减一
            prd.quantity--

            // 如果数量减少到了 0
            if (prd.quantity <= 0) {
                this.cartList = this.cartList.filter(
                    item => item.id !== id
                )
            }
        }
    },
    mounted() {
        event.$on('addToCart', this.addToCart)
        event.$on('delFromCart', this.delFromCart)
    }
}
</script>
// ProductionList
<template>
    <div>
        <ProductionItem
            v-for="item in list"
            :key="item.id"
            :item="item"
        />
    </div>
</template>

<script>
import ProductionItem from './ProductionItem'

export default {
    components: {
        ProductionItem,
    },
    props: {
        list: {
            type: Array,
            default() {
                return [
                    // {
                    //     id: 1,
                    //     title: '商品A',
                    //     price: 10
                    // }
                ]
            }
        }
    }
}
</script>
//ProductionItem.vue
<template>
    <div>
        <span>{{item.title}}</span>
        &nbsp;
        <span>{{item.price}}元</span>
        &nbsp;
        <a href="#" @click="clickHandler(item.id, $event)">加入购物车</a>
    </div>
</template>

<script>
import event from '../event'

export default {
    props: {
        item: {
            type: Object,
            default() {
                return {
                    // id: 1,
                    // title: '商品A',
                    // price: 10
                }
            }
        }
    },
    methods: {
        clickHandler(id, e) {
            e.preventDefault()
            event.$emit('addToCart', id)
        }
    },
}
</script>
//CartList
<template>
    <div>
        <CartItem
            v-for="item in list"
            :key="item.id"
            :item="item"
        />
        <p>总价 {{totalPrice}}</p>
    </div>
</template>

<script>
import CartItem from './CartItem'

export default {
    components: {
        CartItem,
    },
    props: {
        productionList: {
            type: Array,
            default() {
                return [
                    // {
                    //     id: 1,
                    //     title: '商品A',
                    //     price: 10
                    // }
                ]
            }
        },
        cartList: {
            type: Array,
            default() {
                return [
                    // {
                    //     id: 1,
                    //     quantity: 1
                    // }
                ]
            }
        }
    },
    computed: {
        // 购物车商品列表
        list() {
            return this.cartList.map(cartListItem => {
                // 找到对应的 productionItem
                const productionItem = this.productionList.find(
                    prdItem => prdItem.id === cartListItem.id
                )

                // 返回商品信息,外加购物数量
                return {
                    ...productionItem,
                    quantity: cartListItem.quantity
                }
                // 如:
                // {
                //     id: 1,
                //     title: '商品A',
                //     price: 10,
                //     quantity: 1 // 购物数量
                // }
            })
        },
        // 总价
        totalPrice() {
            return this.list.reduce(
                (total, curItem) => total + (curItem.quantity * curItem.price),
                0
            )
        }
    }
}
</script>
//CartItem.vue 
<template>
    <div>
        <span>{{item.title}}</span>
        &nbsp;
        <span>(数量 {{item.quantity}})</span>
        &nbsp;
        <a href="#" @click="addClickHandler(item.id, $event)">增加</a>
        &nbsp;
        <a href="#" @click="delClickHandler(item.id, $event)">减少</a>
    </div>
</template>

<script>
import event from '../event'

export default {
    props: {
        item: {
            type: Object,
            default() {
                return {
                    // id: 1,
                    // title: '商品A',
                    // price: 10,
                    // quantity: 1 // 购物数量
                }
            }
        }
    },
    methods: {
        addClickHandler(id, e) {
            e.preventDefault()
            event.$emit('addToCart', id)
        },
        delClickHandler(id, e) {
            e.preventDefault()
            event.$emit('delFromCart', id)
        }
    }
}
</script>
//TotalPrice.vue
<template>
    <p>total price</p>
</template>

<script>
export default {
    data() {
        return {
        }
    }
}
</script>

结合Vuex实现购物车

//App.vue
<template>
  <div id="app">
    <h1>Shopping Cart Example</h1>
    <hr>
    <h2>Products</h2>
    <ProductList/>
    <hr>
    <ShoppingCart/>
  </div>
</template>

<script>
import ProductList from './ProductList.vue'
import ShoppingCart from './ShoppingCart.vue'

export default {
  components: { ProductList, ShoppingCart }
}
</script>

//ProductList.vue
<template>
  <ul>
    <li
      v-for="product in products"
      :key="product.id">
      {{ product.title }} - {{ product.price | currency }}

      (inventory: {{product.inventory}})<!-- 这里可以自己加一下显示库存 -->
      <br>
      <button
        :disabled="!product.inventory"
        @click="addProductToCart(product)">
        Add to cart
      </button>
    </li>
  </ul>
</template>

<script>
import { mapState, mapActions } from 'vuex'

export default {
  computed: mapState({
    // 获取所有商品
    products: state => state.products.all
  }),
  methods: mapActions('cart', [
    // 添加商品到购物车
    'addProductToCart'
  ]),
  created () {
    // 加载所有商品
    this.$store.dispatch('products/getAllProducts')
  }
}
</script>
//ShoppingCart.vue
<template>
  <div class="cart">
    <h2>Your Cart</h2>
    <p v-show="!products.length"><i>Please add some products to cart.</i></p>
    <ul>
      <li
        v-for="product in products"
        :key="product.id">
        {{ product.title }} - {{ product.price | currency }} x {{ product.quantity }}
      </li>
    </ul>
    <p>Total: {{ total | currency }}</p>
    <p><button :disabled="!products.length" @click="checkout(products)">Checkout</button></p>
    <p v-show="checkoutStatus">Checkout {{ checkoutStatus }}.</p>
  </div>
</template>

<script>
import { mapGetters, mapState } from 'vuex'

export default {
  computed: {
    ...mapState({
      // 结账的状态
      checkoutStatus: state => state.cart.checkoutStatus
    }),
    ...mapGetters('cart', {
      products: 'cartProducts', // 购物车的商品
      total: 'cartTotalPrice' // 购物车商品的总价格
    })
  },
  methods: {
    // 结账
    checkout (products) {
      this.$store.dispatch('cart/checkout', products)
    }
  }
}
</script>
//store
//index.js
import Vue from 'vue'
import Vuex from 'vuex'
import cart from './modules/cart'
import products from './modules/products'
import createLogger from '../../../src/plugins/logger'

Vue.use(Vuex)

const debug = process.env.NODE_ENV !== 'production'

export default new Vuex.Store({
  modules: {
    cart,
    products
  },
  strict: debug,
  plugins: debug ? [createLogger()] : []
})

//mock shop.js
/**
 * Mocking client-server processing
 */
const _products = [
  {"id": 1, "title": "iPad 4 Mini", "price": 500.01, "inventory": 2},
  {"id": 2, "title": "H&M T-Shirt White", "price": 10.99, "inventory": 10},
  {"id": 3, "title": "Charli XCX - Sucker CD", "price": 19.99, "inventory": 5}
]

export default {
  // 获取所有商品,异步模拟 ajax
  getProducts (cb) {
    setTimeout(() => cb(_products), 100)
  },

  // 结账,异步模拟 ajax
  buyProducts (products, cb, errorCb) {
    setTimeout(() => {
      // simulate random checkout failure.
      // 模拟可能失败的情况
      (Math.random() > 0.5 || navigator.userAgent.indexOf('PhantomJS') > -1)
        ? cb()
        : errorCb()
    }, 100)
  }
}

//cart.js
import shop from '../../api/shop'

// initial state
// shape: [{ id, quantity }]
const state = {
  // 已加入购物车的商品,格式如 [{ id, quantity }, { id, quantity }]
  // 注意,购物车只存储 id 和数量,其他商品信息不存储
  items: [],
  // 结账的状态 - null successful failed
  checkoutStatus: null
}

// getters
const getters = {
  // 获取购物车商品
  cartProducts: (state, getters, rootState) => {
    // rootState - 全局 state

    // 购物车 items 只有 id  quantity ,没有其他商品信息。要从这里获取。
    return state.items.map(({ id, quantity }) => {
      // 从商品列表中,根据 id 获取商品信息
      const product = rootState.products.all.find(product => product.id === id)
      return {
        title: product.title,
        price: product.price,
        quantity
      }
    })
  },

  // 所有购物车商品的价格总和
  cartTotalPrice: (state, getters) => {
    // reduce 的经典使用场景,求和
    return getters.cartProducts.reduce((total, product) => {
      return total + product.price * product.quantity
    }, 0)
  }
}

// actions —— 异步操作要放在 actions
const actions = {
  // 结算
  checkout ({ commit, state }, products) {
    // 获取购物车的商品
    const savedCartItems = [...state.items]

    // 设置结账的状态 null
    commit('setCheckoutStatus', null)

    // empty cart 清空购物车
    commit('setCartItems', { items: [] })

    // 请求接口
    shop.buyProducts(
      products,
      () => commit('setCheckoutStatus', 'successful'), // 设置结账的状态 successful
      () => {
        commit('setCheckoutStatus', 'failed') // 设置结账的状态 failed
        // rollback to the cart saved before sending the request
        // 失败了,就要重新还原购物车的数据
        commit('setCartItems', { items: savedCartItems })
      }
    )
  },

  // 添加到购物车
  // 【注意】这里没有异步,为何要用 actions ???—— 因为要整合多个 mutation
  //        mutation 是原子,其中不可再进行 commit !!!
  addProductToCart ({ state, commit }, product) {
    commit('setCheckoutStatus', null) // 设置结账的状态 null

    // 判断库存是否足够
    if (product.inventory > 0) {
      const cartItem = state.items.find(item => item.id === product.id)
      if (!cartItem) {
        // 初次添加到购物车
        commit('pushProductToCart', { id: product.id })
      } else {
        // 再次添加购物车,增加数量即可
        commit('incrementItemQuantity', cartItem)
      }
      // remove 1 item from stock 减少库存
      commit('products/decrementProductInventory', { id: product.id }, { root: true })
    }
  }
}

// mutations
const mutations = {
  // 商品初次添加到购物车
  pushProductToCart (state, { id }) {
    state.items.push({
      id,
      quantity: 1
    })
  },

  // 商品再次被添加到购物车,增加商品数量
  incrementItemQuantity (state, { id }) {
    const cartItem = state.items.find(item => item.id === id)
    cartItem.quantity++
  },

  // 设置购物车数据
  setCartItems (state, { items }) {
    state.items = items
  },

  // 设置结算状态
  setCheckoutStatus (state, status) {
    state.checkoutStatus = status
  }
}

export default {
  namespaced: true,
  state,
  getters,
  actions,
  mutations
}

//products.js 
import shop from '../../api/shop'

// initial state
const state = {
  all: []
}

// getters
const getters = {}

// actions —— 异步操作要放在 actions
const actions = {
  // 加载所有商品
  getAllProducts ({ commit }) {
    // 从 shop API 加载所有商品,模拟异步
    shop.getProducts(products => {
      commit('setProducts', products)
    })
  }
}

// mutations
const mutations = {
  // 设置所有商品
  setProducts (state, products) {
    state.all = products
  },

  // 减少某一个商品的库存(够买一个,库存就相应的减少一个,合理)
  decrementProductInventory (state, { id }) {
    const product = state.all.find(product => product.id === id)
    product.inventory--
  }
}

export default {
  namespaced: true,
  state,
  getters,
  actions,
  mutations
}

总结

data数据结构设计
组件设计组件通讯
结合redux

十二、项目流程

面试题

PM想在项目开发过程中增加需求,该怎么办
项目即将延期了,该怎么办
你将如何保证项目质量

如何讲解

项目的所有角色
项目的全流程
各个阶段中的常见问题

项目角色

PM产品经理
UE视觉设计师
FE前端开发
RD后端开发
CRD移动端开发
QA测试人员

项目流程

一个完整的项目要分哪些阶段 image.png

需求分析

评审项目需求是需要注意哪些事项

  • 了解背景
  • 质疑需求是否合理
  • 需求是否闭环
  • 开发难度如何
  • 是否需要其他支持
  • 不要急于给排期

技术方案分析

如何做好技术方案设计(状态组件设计,接口设计,客户端能力设计,数据输入输出设计)

  • 求简,不过渡设计
  • 产出文档
  • 找准设计重点
  • 组内评审
  • 和RD CRD沟通 发出会议结论

开发

如何保证代码质量

  • 如何反馈排期 (多四分之一的时间)
  • 符合开发规范 1
  • 写出开发文档 2
  • 及时单元测试 3
  • Mock API
  • Code Review 4

联调

和RD CRD技术联调
让UE确定视觉效果
让PM确定产品功能

PM加需求怎么办

不能拒绝,走需求变更流程即可
如果公司有规定,则按规定走
否则,发起项目组合leader的评审,重新评估排期

测试

提测发邮件,抄送项目组
测试问题要详细记录
有问题及时沟通,QA和FE天生信息不对称

我电脑没问题呀

不要说这句话
当面讨论,让QA帮你复现
如果需要特定设备才能复现,让QA提供设备

上线

上线之后及时用纸QA回归测试 上线之后及时同步给PM和项目组 如有问题,及时回滚,先止损,再排查问题

项目沟通

多人协作,沟通是最重要的事情 每日一沟通(如站会),有事说事,无事报平安 及时识别风险,及时汇报

总结

为何考察-确定有项目经验 项目分多阶段 项目需要计划和执行

讲解内容

项目角色
项目流程和阶段
遇到的问题