前端工程化

161 阅读6分钟

历史:WebPage ==> WebApp \

前端工程化

技术选型 --> vue / react / element / ant (手写 --> 浏览器识别 : 工程化转译)
统一规范 --> eslint / husky (代码规范)
测试、部署、监控 --> ut、e2e、mock
性能优化 --> 懒加载、module 拆分 模块化重构 --> SPA、多页、多chunk

WebPack 满足工程化的工具 类似的gulp、jade、pug、grunt

WebPack 面试: 基本配置 -- 场景应用 -- 扩展开发

WebPack 基础使用

image.png

入口:run dev > 查看 package.json 找到 dev build/build.js

  1. 检查版本 build/build.js
require('./check-versions')() // 检查node和npm版本

./check-versions

// 开辟子进程执行cmd指令,并且返回
function exec (cmd) {
  return require('child_process').execSync(cmd).toString().trim()
}

// node和npm版本需求
const versionRequirements = [
  {
    name: 'node',
    currentVersion: semver.clean(process.version),
    versionRequirement: packageConfig.engines.node
  }
]

// 依次判断版本是否符合要求
  for (let i = 0; i < versionRequirements.length; i++) {
    const mod = versionRequirements[i]

    if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
      warnings.push(mod.name + ': ' +
        chalk.red(mod.currentVersion) + ' should be ' +
        chalk.green(mod.versionRequirement)
      )
    }
  }

  // 警告输出
    console.log(chalk.yellow('To use this template, you must update following to modules:'))
    console.log()
  1. 指定环境变量 build/build.js
process.env.NODE_ENV = 'production' // 指定环境变量
  1. loading 插件及其他配置
const ora = require('ora') // 一个美美的loading插件
const rm = require('rimraf') // rm -rf Node版本的unix命令
const path = require('path') // node自带的文件路径插件
const chalk = require('chalk') // 控制台高亮
const webpack = require('webpack')
const config = require('../config') // 配置入口
const webpackConfig = require('./webpack.prod.conf') // webpack配置
  1. config 三套环境 并行并且相互隔离的入口
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
  if (err) throw err
  webpack(webpackConfig, (err, stats) => {
    spinner.stop()
    if (err) throw err

    // webpack编译的开始
    process.stdout.write(stats.toString({
      colors: true,
      modules: false,
      children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
      chunks: false,
      chunkModules: false
    }) + '\n\n')
  1. 开发环境
    5.1 dev 和 base 的配置合并

    webpack.dev.conf.js

    // dev.conf 的配置 和 base.conf的配置合并
    const devWebpackConfig = merge(baseWebpackConfig, {
     module: {
     rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
     },
    

    5.2 本地 server 的配置

    devServer: {
        clientLogLevel: 'warning',
        historyApiFallback: {
          rewrites: [
            { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
          ],
        },
        hot: true,
        contentBase: false, // since we use CopyWebpackPlugin.
        compress: true,
        host: HOST || config.dev.host,
        port: PORT || config.dev.port,
        open: config.dev.autoOpenBrowser,
        overlay: config.dev.errorOverlay
          ? { warnings: false, errors: true }
          : false,
        publicPath: config.dev.assetsPublicPath,
        proxy: config.dev.proxyTable, // 面试题:如何本地项目去做一些多端口服务的代理转发??
        quiet: true, // necessary for FriendlyErrorsPlugin
        watchOptions: {
          poll: config.dev.poll,
        }
    

    面试题:如何本地项目去做一些多端口服务的代理转发??

    通过配置proxyTable (../config/index.js)

    proxyTable: {
          '/api': {
             target: 'http://localhost:8080/',
             pathRewrite: {
               '^/api': '/static/mock'
         }
        },
    

    包括改变端口号

    // Various Dev Server settings
        host: 'localhost', // can be overwritten by process.env.HOST
        port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
        autoOpenBrowser: false,
        errorOverlay: true,
        notifyOnErrors: true,
        poll: false, 
    

    5.3 plugins 微内核:像一个底座,插入各种插件

     plugins: [
     new webpack.DefinePlugin({
       'process.env': require('../config/dev.env')
     }),
     new webpack.HotModuleReplacementPlugin(),
     new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
     new webpack.NoEmitOnErrorsPlugin(),
     // https://github.com/ampedandwired/html-webpack-plugin
     ...
    

    面试常考的插件:

    1. HtmlWebpackPlugin 生产 html 模版文件:
    // 生成html模板文件
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'index.html',
      inject: true
    }),
    

    面试题: 如何利用webpack去做依赖锁定?不想写在npm 中,外部外链一个依赖

    锁依赖固定板本 script

    1. CopyWebpackPlugin 面试题:如何静态文件的移动&赋值
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, '../static'),
        // 打包完成后,这里patch 会配置成 dist 文件夹,打包完成后,会对static下的文件移动到目标文件夹
        to: config.dev.assetsSubDirectory,
        ignore: ['.*']
      }
    ])
    
  2. 生产环境
    6.1 代码压缩

    // 代码压缩
     new UglifyJsPlugin({
       uglifyOptions: {
         compress: {
           warnings: false
         }
       },
       sourceMap: config.build.productionSourceMap,
       parallel: true
     }),
    

    6.2 分trunk app与manifest 分开

    如何通过webpack进行性能优化?

    分trunk 结合路由 进行懒加载,一个路由指定到一个trunk 上,其他的部分不加载,优化了加载速度。

loader 与 plugin

loader: 编译过程中的通道,对编译过程中拿到内容,转换再输出 module - solve
plugin: 编译的生命周期时,有额外功能的拓展

性能优化如何去做?

image.png 2.1 tree shaking -->

// elementUI
import { select, button, tip } from elementUI
Vue.use(select)
Vue.use(button)
Vue.use(tip)

// lodash
import { deepEqual, equal } from lodash
equal(a, b)

通过以上的方式,webpack在打包的时候,不会将整个依赖打包进去,而只会通过tree shaking 将使用的函数打包

2.2 模块懒加载 --> vue-router + trunk

不同的router 加载 不同的 trunk,页面加载某个url的时候就不会加载整个页面的代码

2.3 noParse
2.4 webpack 升级
2.5 文件指纹:chunkhash

JS函数式编程

特点
  1. Vue 3 React 16.8 全面化函数式的推动
  2. 函数式编程可以使得代码但愿相对更佳独立 - tree shaking 过程更加顺畅,更方便做UT
  3. 减少了对this的依赖,减轻了开发人员对于指向问题的困惑
  4. js天生友好函数式:ramda、lodash

概念

  1. 一种抽象运算过程
  2. 函数式的函数并非对于过程运算,是函数的映射,提供一个函数的功能
  3. 幂等 - 相同的输入始终得到相同的输出
纯函数 - 幂等
// 取数组的前三项
let arr = [1,2,3,4,5];

arr.slice(0, 3) // [1,2,3]
arr.slice(0, 3) // [1,2,3]

arr.splice(0, 3) // [1,2,3]
arr.splice(0, 3) // [4,5]
// 这里splice就不再是幂等了,因为结果和执行次数有关,相同输出不会得到相同结果,非纯函数

对于系统的改造

// 不纯的函数
let min = 18;
let limit = age => age > min;
// limit 依赖外部,依赖环境

// 纯的函数
let limit = age => age > 18;

对于大型系统来说,对于外部状态的依赖,会大大的提高系统的复杂性;

  • 问题: 18被硬编码到了函数内部的,造成了功能拓展的局限

高阶函数HOC

定义:

  1. 函数作为参数被传递到另一个函数中
  2. 函数作为返回值被另一个函数返回
let fn = arg => {
  let outer = 'outer';
  let innerFn = () => {
    console.log(outer);
    console.log(arg);
  }
  return innerFn;
}

let closure = fn(18);
// 闭包

面试问题:内存泄露?

尽量不写闭包 如果无法避免闭包,代码层面可以结合生命周期,销毁;或者让函数 = null; 工程化方面,可以放在chunk里,自动垃圾回收

函数柯里化

传递给函数一部分参数用于功能调用,让他返回一个函数去处理剩下的参数

let add = (x, y) => x + y;

// 柯里化后
let add = x => (y => x + y);
let add2 = add(2);
let add200 = add(200);

add2(2); // 2 + 2 add(2)(2)
add200(50); // 200 + 50

// 回到上面的limit,纯函数化
let limit = min => (age => age > min);
let limit18 = limit(18);
limit18(20) //true;

柯里化是一种预加载方式,通过这个方式一方面生成函数,一方面执行函数

  • 问题 包心菜代码的产生h(g(f(x)));

组合

通过更优雅的方式实现纯函数的解耦

let compose = (f, g) => (x => f(g(x)));

let add1 = x => x + 1;
let mul5 = x => x * 5;

compose(mul5, add1)(2); //15

面试题: 数组长度位置的情况下,拿到最后一项

可以用reverse,但是改变了arr,不纯 利用解耦的思想

let compose = (f, g) => (x => f(g(x)));

let first = arr => arr[0];
let reverse = arr => arr.reverse();

let last = compose(first, reverse);
last([1,2,3,4,5]); //5

基于webpack进行开发

  1. loader

    1.1 通用写法 webpack.base.config.js loader

    {
      test: /\.js$/,
      loader: path.resolve(__dirname, 'loaders', 'loaderA')
    }
    // loaders/loaderA.js 
    module.exports = function(content, map, meta) {
      console.log('I\`m loaderA');
      return content;
    }
    

    柯里化先内层函数,后外层,因此在打包的时候先显示最外层的。 => 可以通过 module.exports.pitch 方法改过来

    module.exports.pitch = function() {
      console.log('pitch A');
    }
    

    1.2 同步方式

    module.exports = function(content, map, meta) {
      console.log('I\`m loaderA');
    
      this.callback(null, content, map, meta);
    }
    

    1.3 异步方式

      module.exports = function(content, map, meta) {
        console.log('I\`m loaderA');
    
        const callback = this.async();
        setTimeout(() => {
          callback(null, content);
        }, 1000)
      }
    

    1.4 loader 路径统一管理

    resolveLoader: {
      modules: [
        'node_modules',
        path.resolve(__dirname, 'loaders')
      ]
    }
    
  2. plugins 都是类,使用的时候都会new 一个实例

console.log(compiler.hooks);
// 不同的声明周期
compiler.hooks.emit.tap('Plugin1', Compilation => {
    console.log('hooks.emit.tap');
})
compiler.hooks.afterEmit.tap('Plugin1', Compilation => {
    console.log('hooks.afterEmit.tap');
})
// 不同的函数
// 处理异步函数
compiler.hooks.emit.tap('Plugin1', (Compilation, cb) => {
    setTimeout(() => {
        console.log('hooks.emit ASYNC');
        cb();
    }, 1000)
})
// 处理promise
compiler.hooks.emit.tap('Plugin1', Compilation => {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log('hooks.emit ASYNC');
            resolve();
        }, 1000)
    })
})

module.exports = PluginA;