前端工程化高频面试点解析

1,813 阅读6分钟

1.express.js VS koa.js

  1. 中间件模型不同:express 的中间件模型为线型,而 koa 的为U型(洋葱模型)
  2. 对异步的处理不同:express 通过回调函数处理异步,而 koa 通过generator 和 async/await 使用同步的写法来处理异步,后者更易维护,但彼时 Node.js 对 async 的兼容性和优化并不够好,所以没有流行起来
  3. 功能不同:express 包含路由、渲染等特性,而 koa 只有 http 模块

PS:express 和 koa 的作者都是 TJ 大神

2.nodejs 多进程

  • 进程,是操作系统进行资源调度和分配的基本单位,每个进程都拥有自己独立的内存区域
  • 线程,是操作系统进行运算调度的最小单位,一个进程可以包含多个线程,多线程之间可共用进程的内存数据
  • 一个进程无法直接访问另一个进程的内存数据,除非通过合法的进程通讯
  • 执行一个 nodejs 文件,即开启了一个进程,可以通过 process.pid 查看进程 id
  • 如操作系统是一个工厂,进程就是一个车间,线程就是一个一个的工人
  • JS 是单线程的,即执行 JS 时启动一个进程(如 JS 引擎,nodejs 等),然后其中再开启一个线程来执行
  • 虽然单线程,JS 是基于事件驱动的,它不会阻塞执行,适合高并发的场景

为何需要多进程

  • 现代服务器都是多核 CPU ,适合同时处理多进程。即,一个进程无法充分利用 CPU 性能,进程数要等于 CPU 核数
  • 服务器一般内存比较大,但操作系统对于一个进程的内存分配是有上限的(2G),所以多进程才能充分利用服务器内存

nodejs 开启多进程

  • child_process.fork 可开启子进程执行单独的计算

    • fork('xxx.js') 开启一个子进程
    • 使用 send 发送信息,使用 on 接收信息
  • cluster.fork 可针对当前代码,开启多个进程来执行

扩展:使用 PM2

  • nodejs 服务开启多进程、进程守护,可使用 pm2
  • 全局安装 pm2 yarn global add pm2
  • 增加 pm2 配置文件
  • 修改 package.json scripts

3.loader 和 plugin

其实这些官网介绍的很详细:Loaders | webpack Plugins | webpack

常见loader

  1. babel-loader 将es6 语法转换为es5语法
  2. ts-loader 把 TS 变成 JS,并提示类型错误
  3. markdown-loader 把 markdown 变成 html
  4. html-loader 把 html 变成 JS 字符串
  5. url-loader 处理其他资源,也可以处理图片资源
  6. sass-loader 把 SASS / SCSS 变成 CSS
  7. css-loader 把 CSS 变成 JS 字符串
  8. style-loader 把 JS 字符串变成 style 标签
  9. postcss-loader 把 CSS 变成更优化兼容的 CSS
  10. vue-loader 把单文件组件(SFC)变成 JS 模块
  11. thread-loader 用于多进程打包

常见plugin

  1. html-webpack-plugin 根据指定模板,自动创建html文件,且自动引入外部资源
  2. clean-webpack-plugin 用于清理之前打包的残余文件
  3. mini-css-extract-plugin 用于提取项目中的css为一个单独的文件
  4. SplitChunksPlugin 用于代码分包(Code Split)
  5. DllPlugin + DllReferencePlugin 用于避免大依赖被频繁重新打包,大幅降低打包时间 webpack使用-详解DllPlugin - SegmentFault 思否
  6. eslint-webpack-plugin 用于检查代码中的错误
  7. DefinePlugin 用于在 webpack config 里添加全局变量
  8. copy-webpack-plugin 用于拷贝静态文件到 dist
  9. @babel/polyfill 用于处理JS兼容性问题,例如IE浏览器不支持Promise
  10. optimize-css-assets-webpack-plugin 用于压缩css

二者的区别

webpack本身只能打包js和json

  • loader 是文件加载器,是对指定的资源进行处理,将资源文件从输入到输出之间的一个转换,将资源文件转化为 js 模块
  • plugin 是 webpack 插件,拥有更宽的能力范围,Webpack要求插件必须是一个函数或者是一个包含apply方法的对象,通过在生命周期的钩子中挂载函数实现扩展。插件机制目的是为了增强webpack自动化方面的能力

开发Loader要专注实现资源模块的加载,从而去实现整体项目的打包

loader文件markdown-loader.js

const marked = require('marked')

module.exports = source => {
  // console.log(source)
  // return 'console.log("hello")'
  const html = marked(source)
  console.log(html)
  // 两种导出方式:
  // return `module.exports=${JSON.stringify(html)}`
  return `export default ${JSON.stringify(html)}`
}

webpack.config.js中如何使用:

module: {
    rules: [
      {
        test: /.md$/,
        use: ['html-loader', './markdown-loader.js']
      }
    ]
  }

开发 Plugin 是为了解决除了资源加载以外的其他的一些自动化工作。

开发的插件可以是一个函数或者是一个包含 apply 方法的对象:

class MyPlugin {
  apply (compiler) {
    console.log('MyPlugin 启动')
    compiler.hooks.emit.tap('MyPlugin', compilation => {
      // compilation 可以理解为此次打包的上下文
      for (const name in compilation.assets) {
        // console.log(name) // 文件名
        console.log(compilation.assets[name].source())
        if(name.endsWith('.js')) {
          const contents = compilation.assets[name].source()
          const withoutComments = contents.replace(/\/\*\*+\//g, '')
          compilation.assets[name] = {
            source: () => withoutComments,
            size: () => withoutComments.length
          }
        }
      }
    })
  }
}

如何使用自定义插件:

plugins: [
  new MyPlugin()
]

4.webpack 如何解决开发时的跨域问题

  • 在开发时,我们的页面在 localhost:8080,JS 直接访问后端接口(如 https://yunmu.comhttp://localhost:3000)会报跨域错误。
  • 为了解决这个问题,可以在 webpack.config.js 中添加如下配置
module.exports = {
  //...
    
  devServer: {
	// 请求 /api/users 就会自动被代理到 http://xiedaimala.com/api/users
    proxy: {
      '/api': {
        target: 'http://yunmu.com',
        // 使请求中的 Origin 从 8080 修改为 xiedaimala.com
        changeOrigin: true,
        // 不配置证书且忽略 HTTPS 报错
        secure: false
      },
    },
  },
};

5.如何实现 tree-shaking

tree-shaking 就是让没有用到的 JS 代码不打包,以减小包的体积

同时满足两个条件 webpack 会自动开启 tree-shaking

  1. 使用 ES Module 语法,CommonJS 语法无法 tree-shaking
  2. 开启 production 环境

可以配置 sideEffects 防止默写文件被 tree-shaking 掉

  • 比如我 import 了 x.js,而 x.js 只是添加了 window.x 属性,那么 x.js 就要放到 sideEffects 里
  • 比如所有被 import 的 CSS 都要放在 sideEffects 里

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

构建性能 | webpack 中文文档 (docschina.org)

  1. 使用 DllPlugin 将不常变化的代码提前打包,并复用,如 vue、react
  2. 使用 thread-loader 或 HappyPack(过时)进行多线程打包
  3. ParallelUglifyPlugin 多进程压缩JS
  4. 处于开发环境时,在 webpack config 中将 cache 设为 true
  5. 处于生产环境时,关闭不必要的环节,比如可以关闭 source map
  6. 优化 babel-loader 开启缓存,include 和 exclude 确定范围
  7. IgnorePlugin 避免引入无用模块
  8. noParse 避免重复打包

7.Vite VS Webpack

  • Vite 开发环境使用 <script type=module> 引入文件,当请求某个文件的时候才会处理该文件,生产环境使用 rollup + esbuild 来打包 JS 代码
  • webpack 开发环境 webpack-dev-server 会使用 babel-loader 将代码递归的打包进内存里然后再进行请求,生产环境使用 babel 来打包 JS 代码

PS:Vite热更常常失败,有些功能需要自己写 rollup 插件,并且不支持现代浏览器

8.webpack 配置多页应用

对应的 webpack config:

const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    app: './src/app.js',
    admin: './src/admin.js',
  },
  plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html',
      chunks: ['app']
    }),
    new HtmlWebpackPlugin({
      filename: 'admin.html',
      chunks: ['admin']
    })
  ],
};

这样打包会产生一个重复打包的问题

假如 app.js 和 admin.js 都引入了 vue.js,那么 vue.js 的代码既会打包进 app.js,也会打包进 admin.js

我们需要使用 optimization.splitChunks 将共同依赖单独打包成 common.js(HtmlWebpackPlugin 会自动引入 common.js)

支持无限多的页面:

const HtmlWebpackPlugin = require('html-webpack-plugin');
const fs = require('fs')
const path = require('path')

const filenames = fs.readdirSync('./src/pages')
  .filter(file => file.endsWith('.js'))
  .map(file => path.basename(file, '.js'))

const entries = filenames.reduce((result, name) => (
  { ...result, [name]: `./src/pages/${name}.js` }
), {})

const plugins = filenames.map((name) =>
  new HtmlWebpackPlugin({
    filename: name + '.html',
    chunks: [name]
  })
)

module.exports = {
  entry: {
    ...entries
  },
  plugins: [
    ...plugins
  ],
};

9.swc、esbuild 是什么?

swc

实现语言:Rust

功能:编译 JS/TS、打包 JS/TS

优势:比 babel 快很多很多很多(20倍以上)

能否集成进 webpack:能

使用者:Next.js、Parcel、Deno、Vercel、ByteDance、Tencent、Shopify……

做不到:

  1. 对 TS 代码进行类型检查(用 tsc 可以)

  2. 打包 CSS、SVG

esbuild

实现语言:Go

功能:编译 JS/TS、打包 JS/TS

优势:比 babel 快很多很多很多很多很多很多(10~100倍)

能否集成进 webpack:能

使用者:vite、vuepress、snowpack、umijs、blitz.js 等

做不到:

  1. 对 TS 代码进行类型检查

  2. 打包 CSS、SVG

10.webpack优化产出代码

  • 小图片base64编码
  • 提取公共代码
  • bundle 加hash
  • 使用CDN加速
  • 懒加载
  • IgnorePlugin
  • 使用production
  • Scope Hosting

11.ES6 Module和 Commonjs区别

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

12.使用 Webpack 实现 Vue 项目打包任务

webpack.common.js

const path = require('path');
const webpack = require('webpack');
const { VueLoaderPlugin } = require('vue-loader');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
  entry: path.join(__dirname, 'src/main.js'),
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
  },
  resolve: {
    extensions: ['.js', '.json', '.css', '.vue'],
  },
  devServer: {
    contentBase: path.join(__dirname, 'public'),
    port: 5000,
    hot: true,
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
      },
      {
        test: /\.js$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
      },
      {
        test: /\.css$/,
        use: ['vue-style-loader', 'css-loader'],
      },
      {
        test: /\.less$/,
        use: ['style-loader', 'css-loader', 'less-loader'],
      },
      {
        test: /.png$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 10 * 1024, // 单位是字节 10KB
            esModule: false,
          },
        },
      },
    ],
  },
  plugins: [
    new VueLoaderPlugin(),
    new HtmlWebpackPlugin({
      title: 'Jal-Vue-Webpack',
      template: './public/index.html',
      filename: 'index.html',
    }),
    new webpack.DefinePlugin({
      BASE_URL: JSON.stringify('/'),
    }),
  ],
};

Webpack.dev.js

const common = require('./webpack.common');
const merge = require('webpack-merge');

module.exports = merge(common, {
  mode: 'development',
  optimization: {
    usedExports: true,
    minimize: true,
  },
  devtool: 'eval-cheap-module-source-map',
});

Webpack.prod.js

const path = require('path');
const merge = require('webpack-merge');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const common = require('./webpack.common');

module.exports = merge(common, {
  mode: 'production',
  output: {
    filename: '[name]-[contenthash:8].bundle.js',
    path: path.join(__dirname, 'dist'),
  },
  devtool: 'none',
  plugins: [
    new CleanWebpackPlugin(),
    new CopyWebpackPlugin({
      patterns: ['public'],
    }),
  ],
});

13.webpack的打包构建流程

  • 连接: webpack从入口JS开始,递归查找出所有相关的模块, 并【连接】起来形成一个图(网)的结构
  • 编译: 将JS模块中的模块化语法【编译】为浏览器可以直接运行的模块语法(当然其它类型资源也会处理)
  • 合并: 将图中所有编译过的模块【合并】成一个或少量的几个文件, 浏览器真正运行是打包后的文件

14.live-reload(自动刷新)与HMR(热模替换)

  • 相同点: 代码修改后都会自动重新编译打包
  • 不同点:
    • live-reload: 刷新整体页面,从而查看到最新代码的效果,页面状态全部都是新的
    • HMR: 没有刷新整个页面,只是加载了修改模块的打包文件并运行,从而更新页面的局部界面,整个界面的其它部分的状态还在