# 大前端培训课程笔记

标签(空格分隔): es6 ts node vue react

第一阶段:JS深度剖析

模块一:ES 新特性与 JS 异步编程、TypeScript

任务一: ES新特性

  • for循环两层嵌套作用域,for块本身一层,内部循环体一层。
  • 最佳实践:不用var,主用const,配合let。
  • 对象解构可设别名和默认值: {name: objName = "jack"} = obj
  • 带标签的模板字符串: func`${name} is a ${gender}` ,作用如在func中做gender的翻译处理。
  • ES6字符串实用扩展方法:startsWith()、endsWith()、includes()。
  • 函数形参设默认值最好放在最后,若不是最后,调函数传参必须传入undefined才能使默认值生效。
  • foo(first, ...args) ,..args表示剩余参数->数组,只能出现在参数列表的最后。
  • arr2 = [...arr1] 和slice()类似,是对没有引用对象元素的数组的深拷贝,但是有的话是浅拷贝。
  • 对象字面量增强:a.变量名属性名相同,写一遍加逗号;b.属性方法直接func()定义;c.对象动态属性用[]直接定义(计算属性名)。
  • Object.assign(target, source1, source2); Object.is(NaN, NaN);
  • Proxy的handler方法:(Reflect api雷同)
  • Proxy 对比 Object.defineProperty()的优势:a.Proxy 可以监视读写以外的操作;b.Proxy 可以很方便的监视数组操作;c.Proxy 不需要侵入对象。
  • 对象中的键只能是字符串(ES6后还可以是Symbol()),但Map中的键可以是任意类型。
  • Symbol()最主要的作用就是为对象添加独一无二的属性名,作为对象的私有属性。
  • in 、Object.keys()、Json.stringfy()均会忽略对象的Symbol类型的属性。
  • 所有可以用for...of循环的结构对象,内部都调用了一个Symbol.iterator方法。
  • 迭代器作用:对外提供统一遍历接口(for...of),外部不用关系对象内部数据结构。
  • 生成器:* function,用yield返回值,用.next()调用。
  • ES2016: a.新方法:array.includes(); b.指数运算符:**。
  • ES2017: Object.values(obj); Object.entries(obj); Object.getOwnPropertyDescriptors(); Object.defineProperties({}, descriptors) String.prototype.padStart(); String.prototype.padEnd()
    async/await
  • 任务二:JavaScript异步编程

    Promise写一个ajax:

    function ajax(url, method) {
      return new Promise(function(resolve, reject) {
        let xhr = new XMLHttpRequest();
        xhr.open(method, url);
        xhr.responseType = "json";
        xhr.onload = function() {
          if (this.status === 200) {
            resolve(this.response);
          } else {
            reject(new Error(this.statusText));
        xhr.send();
    ajax("api/test.json", "get").then(function(value) {
      console.log(value);
    }, function(error) {
      console.log(error);
    

    Promise最常见的错误是嵌套使用(回调地域)。

    Promise链式调用:Promise对象的then()方法会返回一个全新的Promise对象,后面的then方法就是在为上一个then返回的Promise注册回调,前面then方法中回调函数的返回值会作为后面then方法返回回调的参数,如果回调中返回的是Promise,那后面then方法的回调会等待它的结束。

    Promise静态方法(直接返回一个Promise对象): Promise.resolve().then() / Promise.reject().catch()

    Promise.then 或者 .catch 的参数期望是函数,传入非函数则会发生值穿透,如下题:112ede6599390ca5a91f17fb60acc9e.png-7.3kB

    Promise.all()内部的全部异步执行完毕后then,Promise.race()内部的异步谁先执行完就then或者catch返回谁(用于实现处理请求超时)。

    微任务会在当前线程执行完后立即执行,而宏任务要等线程和微任务都结束后才执行,目前绝大多数异步调用是宏任务,Promise是微任务。

    Generator生成器异步方案:

    // 执行生成器的公用函数
    function co(generator) {
      // 迭代生成器方法
      function handleResult(result) {
        if (result.done) {
          return;// 生成器函数结束
        result.value.then(data => {
          handleResult(g.next(data));
        }, error => {
          g.throw(error);
      const g = generator();
      handleResult(g.next());
    // 自定义生成器
    function * main() {
        const data1 = yield ajax("/api/test.json", "get");
        console.log(data1);
        const data3 = yield ajax("/api/error.json", "get");
        console.log(data3);
        const data2 = yield ajax("/api/info.json", "get");
        console.log(data2);
      } catch(e) {
        console.log("报错信息:", e);
    // 执行生成器方法,内部的异步请求将按顺序依次往下执行
    co(main);
    

    任务三:TypeScript语言

    1. 强类型语言的优势(弱类型的缺点):a.错误更早暴露;b.代码更智能;c.重构更牢靠;d.减少不必要的类型判断。
    2. Flow:javacript的类型检查器。Flow安装:yarn add flow-bin --dev; 生成Flow配置文件:yarn flow init; 启动/停止Flow服务:yarn flow / yarn flow stop; Flow编译移除注解:a.flow-remove-types;b.babel/perset-flow。
    3. ts中文错误提示: yarn tsc --locale zh-CN。
    4. 解决作用域问题:a.放到一个立即执行函数中;b.末尾添加 export {}。
    5. typescript命令:a.安装:yarn add typescript;b.配置文件:yarn tsc --init;c.编译:yarn tsc;d.调试:yarn add nodemon/yarn add ts-node/yarn nodemon xxx.ts
    6. typescript数组类型例子:
    
     function sum(...args: number[]) {
        return args.reduce((prev, current) => prev + current, 0);
     sum(1, 2, 3);
    
  • typescript类型总结(示例):
  • // 【原始数据类型】
    const a: string = 'foobar'
    const b: number = 100 // NaN Infinity
    const c: boolean = true // false
    // 在非严格模式(strictNullChecks)下,
    // string, number, boolean 都可以为空
    // const d: string = null
    // const d: number = null
    // const d: boolean = null
    const e: void = undefined
    const f: null = null
    const g: undefined = undefined
    // Symbol 是 ES2015 标准中定义的成员,
    // 使用它的前提是必须确保有对应的 ES2015 标准库引用
    // 也就是 tsconfig.json 中的 lib 选项必须包含 ES2015
    const h: symbol = Symbol()
    // -------------------------------------------------------------------------
    // 【Object 类型】
    // object 类型是指除了原始类型以外的其它类型
    const foo: object = function () {} // [] // {}
    // 如果需要明确限制对象类型,则应该使用这种类型对象字面量的语法,或者是「接口」
    const obj: { foo: number, bar: string } = { foo: 123, bar: 'string' }
    // -------------------------------------------------------------------------
    // 【数组类型】
    const arr1: Array<number> = [1, 2, 3]
    const arr2: number[] = [1, 2, 3]
    // -------------------------------------------------------------------------
    // 【元组类型】
    // 元组类型是指明确每一个元素类型和元素数量的数组,Object.entries()返回的就是一个元组类型
    const tuple: [number, string] = [18, 'zce']
    const entries: [string, number][] = Object.entries({
      foo: 123,
      bar: 456
    const [key, value] = entries[0];
    // 打印entries: [["foo", 123], ["bar", 456]]
    // -------------------------------------------------------------------------
    // 【枚举类型】
    enum PostStatus { // 这种枚举会造成编译时的代码入侵
      Draft = 1,
      Unpublished = 2,
      Published = 3
    const enum PostStatus { // 常量枚举,不会侵入编译结果
      Draft = 1,
      Unpublished,
      Published
    // -------------------------------------------------------------------------
    // 【函数类型】
    function func1 (a: number, b: number = 10, ...rest: number[]): string {
      return 'func1'
    func1(100, 200, 300, 400)
    const func2: (a: number, b: number) => string = function (a: number, b: number): string {
      return 'func2'
    // -------------------------------------------------------------------------
    // 【接口】
    interface Post {
      title: string
      content: string
    function printPost (post: Post) {
      console.log(post.title)
      console.log(post.content)
    printPost({
      title: 'Hello TypeScript',
      content: 'A javascript superset'
    
  • 类型断言:类型断言不是类型转换,它是用来明确某一个变量的具体类型,其方式有两种:
  • const num = res as number;
    const num2 = <number>res // JSX下不能使用
    
  • interface接口的作用在于约束一个对象的结构,一个对象要实现一个接口,就必须拥有接口中约束的所有成员。
  • interface Post {
      title: string
      subtitle?: string // 可选成员
      readonly summary: string // 只读成员
    const hello: Post = {
      title: 'Hello TypeScript',
      summary: 'A javascript'
    interface Cache {
      [prop: string]: string //动态成员
    const cache: Cache = {}
    cache.foo = 'value1'
    
  • ts中的类对es6的类进行了语法上的增强,跟java很像,如下:
  • class Person {
      public name: string // = 'init name'
      private age: number // private表示外部不可访问
      // protected表示外部不可以但是子类可以访问;readonly表示不可修改且赋值只能在初始化或构造器
      protected readonly gender: boolean
      constructor (name: string, age: number) {
        this.name = name
        this.age = age
        this.gender = true
      sayHi (msg: string): void {
        console.log(`I am ${this.name}, ${msg}`)
        console.log(this.age)
    class Student extends Person {
      private constructor (name: string, age: number) { // 构造器私有表示不能外部实例化
        super(name, age)
        console.log(this.gender)
      static create (name: string, age: number) { // 使用静态方法创造实例
        return new Student(name, age)
    const jack = Student.create('jack', 18)
    
  • 类的接口与实现:
  • interface Eat {
      eat (food: string): void
    class Person implements Eat{
      eat (food: string): void {
        console.log(`优雅的进餐: ${food}`)
    
  • 抽象类/方法:class/方法名前加abstract:
  • abstract class Animal {
      eat (food: string): void {
        console.log(`呼噜呼噜的吃: ${food}`)
      abstract run (distance: number): void
    
  • 抽象类和接口的区别:在typescript中接口和抽象类有什么区别
  • // function createNumberArray (length: number, value: number): number[] {
    //   const arr = Array<number>(length).fill(value)
    //   return arr
    // function createStringArray (length: number, value: string): string[] {
    //   const arr = Array<string>(length).fill(value)
    //   return arr
    function createArray<T> (length: number, value: T): T[] {
      const arr = Array<T>(length).fill(value)
      return arr
    
  • Lodash是一个著名的javascript原生库,不需要引入其他第三方依赖。是一个意在提高开发者效率,提高JS原生方法性能的JS库(另有query-string,这些之后要去项目中学习使用起来)。
  • typescript中引入第三方模块的时候,可以:a.自己手动用declare声明模块;b.用yarn add @types/模块名 添加声明;c.第三方库有可能已经自己集成兼容ts了。
  • 模块二:函数式编程与 JavaScript 性能优化

    任务一:函数式编程范式

    函数是一等公民是指,函数可以作为参数、可以作为返回值、可以赋值给变量。

    高阶函数--函数作为参数(回调):

    // array的forEach()、filter()、some()、map()、every()等方法,都是高阶函数
    // 模拟foreach
    function forEach(arr, fn) {
      for (let i = 0; i < arr.length; i++) {
        fn(arr[i]);
    let arr = [2,3,5,8,12];
    forEach(arr, item => console.log(item));
    // 模拟filter
    function filter(arr, fn) {
      let results = [];
      for (let i = 0; i < arr.length; i++) {
        if (fn(arr[i])) {
          results.push(arr[i]);
      return results;
    let arr2 = [2,3,5,8,12];
    console.log(filter(arr2, item => item % 2 === 0));
      return function() {
        let key = JSON.stringify(arguments)
        cache[key] = cache[key] || fn.apply(this, arguments)
        return cache[key]
    function getCircleArea(r) {
      console.log("first exct");
      return Math.PI * r * r
    let getAreaWithMemory = memoize(getCircleArea);
    console.log(getAreaWithMemory(4)); // 会打印"first exct"
    console.log(getAreaWithMemory(4)); // 已有缓存,不会重复执行
        if (args.length < fn.length) {
          // return function () {
          //   return curriedFn(...args.concat(Array.from(arguments)))
          return function (...rest) {
            return curriedFn(...args.concat(rest))
        return fn(...args)
    
    // es6写法
    const curry = fn => curriedFn = (...args) => args.length < fn.length ? (...rest) => curriedFn(...args.concat(rest)) : fn(...args)
    const getSum = (a, b, c) => a + b + c;
    const getSumCurried = curry(getSum);
    console.log(getSumCurried(1)(2)(3));
    function compose(...args) {
      return function (value) {
        return args.reverse().reduce(function(acc, fn) {
          return fn(acc)
        }, value)
    // es6写法
    // const compose = (...args) => value => args.reverse().reduce((acc, fn) => fn(acc), value);
    const reverse = arr => arr.reverse();
    const first = arr => arr[0];
    const fn = compose(first, reverse);
    console.log(fn(["jack", "tom", "rose"]));
    

    PointFree:只需要定义一些辅助的基本运算函数,然后合成运算过程,不需要指明处理的数据,pointfree案例如下:

    // 把一个字符串中的首字母提取并转换成大写, 使用. 作为分隔符
    // world wild web ==> W. W. W
    const fp = require('lodash/fp')
    // const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.first), fp.map(fp.toUpper), fp.split(' '))
    const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.flowRight(fp.first, fp.toUpper)), fp.split(' '))
    console.log(firstLetterToUpper('world wild web'))
    

    函子(Functor):是一个特殊的容器,通过一个普通的对象来实现,该对象具有map方法,map方法可以运行一个函数对值进行处理(变形关系)。可以用来在函数式编程中把副作用控制在可控的范围内、异常处理、异步操作等。

    基本函子:

    // Pointed函子:实现了of静态方法的函子
    class Container {
      static of (value) {
        return new Container(value)
      constructor (value) {
        this._value = value
      map (fn) {
        return Container.of(fn(this._value))
    // let r = Container.of(5)
    //           .map(x => x + 2)
    //           .map(x => x * x)
    // console.log(r)
    

    IO函子,中的_value是一个函数,这里是把函数作为值来处理,可以把不纯的动作存储到 _value中,用来延迟这个不纯的操作,把不纯的操作交给调用者来处理:

    const fp = require('lodash/fp')
    class IO {
      static of (value) {
        return new IO(function () {
          return value
      constructor (fn) {
        this._value = fn
      map (fn) {
        return new IO(fp.flowRight(fn, this._value))
    let r = IO.of(process).map(p => p.execPath)
    console.log(r._value())
    

    Folktale:一个标准的函数式编程库,和loadsh、remda不同,没有提供很多功能函数,而是只提供了一些函数式处理(如compose、curry)和一些函子(Task、Either、MayBe等)。

    Task函子(处理异步任务):

    // Task 处理异步任务
    const fs = require('fs')
    const { task } = require('folktale/concurrency/task')
    const { split, find } = require('lodash/fp')
    function readFile (filename) {
      return task(resolver => {
        fs.readFile(filename, 'utf-8', (err, data) => {
          if (err) resolver.reject(err)
          resolver.resolve(data)
    readFile('package.json')
      .map(split('\n'))
      .map(find(x => x.includes('version')))
      .run()
      .listen({
        onRejected: err => {
          console.log(err)
        onResolved: value => {
          console.log(value)
    

    Monad函子:可以变扁的Pointed函子,具有join()和of()两个方法,可以用来解决IO函子嵌套过多的问题。

    // IO Monad
    const fs = require('fs')
    const fp = require('lodash/fp')
    class IO {
      static of (value) {
        return new IO(function () {
          return value
      constructor (fn) {
        this._value = fn
      map (fn) {
        return new IO(fp.flowRight(fn, this._value))
      join () {
        return this._value()
      flatMap (fn) {
        return this.map(fn).join()
    let readFile = function (filename) {
      return new IO(function () {
        return fs.readFileSync(filename, 'utf-8')
    let print = function (x) {
      return new IO(function () {
        console.log(x)
        return x
    let r = readFile('package.json')
              // .map(x => x.toUpperCase())
              .map(fp.toUpper)
              .flatMap(print)
              .join()
    

    标记清除原理:遍历并标记活动对象,遍历并清除没有标记的对象。优点:可以解决循环引用不能回收的问题。缺点:回收后的空间是碎片化的,不能使空间得到最大化的使用;不会立即回收垃圾对象。

    标记整理原理:标记清除的增强,标记阶段一致,清除阶段会先执行整理,移动对象位置。优点:可以解决空间碎片化问题。缺点:不会立即回收垃圾对象。

    V8是一个js引擎,特点是采用即时编译和内存设限。

    V8垃圾回收策略:分代回收、空间复制(新生代)、标记整理(新、老生代)、标记清除(老生代)、标记增量(老生代)。

    界定内存问题的标准:a.内存泄漏:内存使用持续升高;b.内存膨胀:在多数设备上都存在性能问题;c.频繁的垃圾回收:通过内存变化图进行分析。

    监控内存的几种方式:a.浏览器任务管理器;b.timeline时序图记录;c.堆快照查找分离dom;d.判断是否存在频繁的垃圾回收。

    基于Benchmark.js的Jsperf可以进行js性能的单元测试。

    js性能优化策略:

    // 【1.慎用全局变量】
    function fn() {
      // name = 'lg'
      const name = 'lg'
      console.log(`${name} is a coder`)
    // 【2.缓存全局变量】
    function getBtn2() {
      let obj = document
      let oBtn1 = obj.getElementById('btn1')
    // 【3.通过原型对象添加附加方法】
    var fn1 = function() {
      // this.foo = function() {
      // 	console.log(11111)
    fn1.prototype.foo = function() {
      console.log(11111)
    // 【4.避开闭包陷阱】
    // 【5.避免属性访问方法使用】
    function Person() {
      this.name = 'icoder'
      this.age = 18
      // this.getAge = function() {
      //   return this.age
    const p2 = new Person()
    // const a = p1.getAge()
    const a = p2.age
    // 【6.for循环优化】
    for (var i = 0,len = arrList.length; i < len; i++) {
      console.log(arrList[i])
    // 【7.选择最优循环方法】
    // foreach > for > forin
    // 【8.文档碎片优化节点添加】
    var oP = document.createElement('p')
    // document.body.appendChild(oP)
    const fragEle = document.createDocumentFragment()
    fragEle.appendChild(oP)
    document.body.appendChild(fragEle)
    // 【9.克隆优化节点操作】
    var oldP = document.getElementById('box1')
    for (var i = 0; i < 3; i++) {
      // var newP = document.createElement('p')
      var newP = oldP.cloneNode(false)
      newP.innerHTML = i
      document.body.appendChild(newP)
    // 【10.直接量替换new object()】
    

    前端工程化主要解决的问题:

    (1) 传统语言或语法的弊端;(2) 无法使用模块化/组件化;(3) 重复的机械式工作;

    (4) 代码风格统一、质量保证;(5) 依赖后端服务接口支持;(6) 整体依赖后端项目;

    工程化表现:一切以提高效率、降低成本、质量保证为目的的手段都属于工程化;

    任务二:脚手架工具

    node的环境变量配置

    在node安装路径下新建node_global、node_cache文件夹;

    在终端运行:

    npm config set prefix "D:\Node\nodejs\node_global"
    npm config set cache "D:\Node\nodejs\node_cache"
    

    (1) package.json中定义:"bin":"test-cli.js"

    (2) 根目录建test-cli.js文件和templates模板文件夹

    (3) 编写test-cli.js:

    #!/usr/bin/env node
    // Node CLI 应用入口文件必须要有这样的文件头
    // 如果是 Linux 或者 macOS 系统下还需要修改此文件的读写权限为 755
    // 具体就是通过 chmod 755 cli.js 实现修改
    // 脚手架的工作过程:
    // 1. 通过命令行交互询问用户问题
    // 2. 根据用户回答的结果生成文件
    const fs = require('fs')
    const path = require('path')
    const inquirer = require('inquirer')
    const ejs = require('ejs')
    inquirer.prompt([
        type: 'input',
        name: 'name',
        message: 'Project name?'
    .then(anwsers => {
      // console.log(anwsers)
      // 根据用户回答的结果生成文件
      // 模板目录
      const tmplDir = path.join(__dirname, 'templates')
      // 目标目录
      const destDir = process.cwd()
      // 将模板下的文件全部转换到目标目录
      fs.readdir(tmplDir, (err, files) => {
        if (err) throw err
        files.forEach(file => {
          // 通过模板引擎渲染文件
          ejs.renderFile(path.join(tmplDir, file), anwsers, (err, result) => {
            if (err) throw err
            // 将结果写入目标文件路径
            fs.writeFileSync(path.join(destDir, file), result)
      "scripts": {
        "build": "sass scss/main.scss css/style.css --watch",
        "serve": "browser-sync . --files \"css/*.css\"",
        "start": "run-p build serve"
    

    yarn start

    常用的自动化构建工具:Grunt(生态完善,但基于临时文件、构建速度慢)、Gulp(基于内存、构建速度快、效率高、更方便)、FIS(百度推出,集成比较多,大而全)。

    Grunt的使用:

    yarn init --yes

    yarn add grunt

    code gruntfile.js

    // gruntfile.js
    // Grunt 的入口文件
    // 用于定义一些需要 Grunt 自动执行的任务
    // 需要导出一个函数
    // 此函数接收一个 grunt 的对象类型的形参
    // grunt 对象中提供一些创建任务时会用到的 API
    module.exports = grunt => {
      grunt.registerTask('foo', 'a sample task', () => {
        console.log('hello grunt')
      grunt.registerTask('bar', () => {
        console.log('other task')
      // // default 是默认任务名称
      // // 通过 grunt 执行时可以省略
      // grunt.registerTask('default', () => {
      //   console.log('default task')
      // })
      // 第二个参数可以指定此任务的映射任务,
      // 这样执行 default 就相当于执行对应的任务
      // 这里映射的任务会按顺序依次执行,不会同步执行
      grunt.registerTask('default', ['foo', 'bar'])
      // 也可以在任务函数中执行其他任务
      grunt.registerTask('run-other', () => {
        // foo 和 bar 会在当前任务执行完成过后自动依次执行
        grunt.task.run('foo', 'bar')
        console.log('current task runing~')
      // 默认 grunt 采用同步模式编码
      // 如果需要异步可以使用 this.async() 方法创建回调函数
      // grunt.registerTask('async-task', () => {
      //   setTimeout(() => {
      //     console.log('async task working~')
      //   }, 1000)
      // })
      // 由于函数体中需要使用 this,所以这里不能使用箭头函数
      grunt.registerTask('async-task', function () {
        const done = this.async()
        setTimeout(() => {
          console.log('async task working~')
          done()
        }, 1000)
    
  • yarn grunt async-task
  • grunt.registerMultiTask('build', function () { console.log(`task: build, target: ${this.target}, data: ${this.data}`)

    Grunt常用插件使用:sass、babel、watch:

    const sass = require('sass')
    const loadGruntTasks = require('load-grunt-tasks')
    module.exports = grunt => {
      grunt.initConfig({
        sass: {
          options: {
            sourceMap: true,
            implementation: sass
          main: {
            files: {
              'dist/css/main.css': 'src/scss/main.scss'
        babel: {
          options: {
            sourceMap: true,
            presets: ['@babel/preset-env']
          main: {
            files: {
              'dist/js/app.js': 'src/js/app.js'
        watch: {
          js: {
            files: ['src/js/*.js'],
            tasks: ['babel']
          css: {
            files: ['src/scss/*.scss'],
            tasks: ['sass']
      // grunt.loadNpmTasks('grunt-sass')
      loadGruntTasks(grunt) // 自动加载所有的 grunt 插件中的任务
      grunt.registerTask('default', ['sass', 'babel', 'watch'])
    
    // gulpfile.js
    const { src, dest } = require('gulp')
    const cleanCSS = require('gulp-clean-css')
    const rename = require('gulp-rename')
    exports.default = () => {
      return src('src/*.css')
        .pipe(cleanCSS())
        .pipe(rename({ extname: '.min.css' }))
        .pipe(dest('dist'))
    
  • 通过给script添加type="module"就可以以ES Module的标准执行其中的JS代码;
  • ESM自动采用严格模式,忽略 'use strict'(不能全局使用this);
  • 每个ESM都是运行在单独的私有作用域中;
  • ESM是通过CORS的方式请求外部JS模块的;
  • ESM的script标签会延迟执行脚本;
  • ES Modules导出导入: export / import。

    热更新模块:browser-sync;

    ES Modules 导出的注意事项:

  • export / import后面的{}是固定语法,与es6字面对象和结构无关;
  • export导出的是变量的引用,不是变量的复制;
  • export导出的是变量是只读的,不可在模块外部修改;
  • ES Modules 导入的注意事项:

    import from后的路径和名称必须是完整的,不可省略.js和目录路径;

    相对路径的话不可省略./,且可使用绝对路径或者完整的url;

    import './module.js'只执行相关的模块文件,不导入变量;

    import * as modObj from './module.js' 可以全部导出;

    import不可嵌套在函数中,不可from一个变量,如有此动态加载需求,需使用:

    import('.module.js').then(function(module){
        console.log(module);
    

    若export同时导出命名成员和默认成员,import可以使用如下方式接收:

    // import {name, age, default as title} as modObj './module.js'
    import title, {name, age} as modObj './module.js'
    

    Webpack默认从src下的index.js打包到dist下的main.js,可在根目录添加webpack.config.js进行自定义:

    // webpack.config.js
    const path = require('path')
    module.exports = {
        // mode有:development/production/none
        mode: 'development',
        entry: './src/main.js',
        output: {
            filename: 'bundle.js',
            path: path.join(__dirname, 'output'),
            publicPath: 'dist/'
    

    Webpack 资源模块加载:加载除了js外的资源模块,需要使用module加载loader,Loader是Webpack的核心特性。

    // webpack.config.js
    module: {
        rules: [
                test: /.css$/,
                use: [
                    'style-loader',
                    'css-loader'
          // 小文件使用Data URLS,减少请求次数 ---> url-loader
          // 大文件单独存放,提高加载速度 ---> file-loader
          limit: 10 * 1024 // 10 KB
    

    webpack加载资源的方式支持遵循ESM的import、CommonJs的require、AMD的define和require、css-loader中的@import和url、html代码中的img:src和a:herf。

    对于同一个资源,可以依次使用多个loader,Loaders类似一个管道,最终接收的是一个javascript格式的字符串。

    Webpack 插件机制 :Loader专注于实现资源模块加载,而Plugin解决其他自动化工作,Plugin拥有更宽的能力范围。

    常用的一些webpack插件:

  • 自动清除输出目录插件:clean-webpack-plugin
  • 自动生成Html插件:html-webpack-plugin
  • 复制文件插件:copy-webpack-plugin
  • // webpack.config.js 
    const { CleanWebpackPlugin } = require('clean-webpack-plugin')
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    const CopyWebpackPlugin = require('copy-webpack-plugin')
    plugins: [
        new CleanWebpackPlugin(),
        // 用于生成 index.html
        new HtmlWebpackPlugin({
          title: 'Webpack Plugin Sample',
          meta: {
            viewport: 'width=device-width'
          template: './src/index.html'
        // 用于生成 about.html
        new HtmlWebpackPlugin({
          filename: 'about.html'
        new CopyWebpackPlugin([
          // 'public/**'
          'public'
    

    自己实现webpack插件,需要通过在生命周期的钩子中挂载函数实现扩展,如要实现一个去除打包后js文件中的开头的注释的插件MyPlugin:

    // webpack.config.js 
    class MyPlugin {
      apply (compiler) {
        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
    module.exports = {
      mode: 'none',
      entry: './src/main.js',
      output: {},
      module: {
        rules: []
      plugins: [
        new MyPlugin()
          '/api': {
            // http://localhost:8080/api/users -> https://api.github.com/api/users
            target: 'https://api.github.com',
            // http://localhost:8080/api/users -> https://api.github.com/users
            pathRewrite: {
              '^/api': ''
            // 不能使用 localhost:8080 作为请求 GitHub 的主机名
            changeOrigin: true
    

    Webpack HMR除了css文件外,需要手动去入口文件处理热更新逻辑:

    module.hot.accept('./better.png', () => {
        img.src = background
        console.log(background)
    

    配置文件根据环境导出不同配置,webpack.config.js导出一个函数:

    module.exports = (env, argv) => {
    const config = {}, // 放一些公共配置
        plugins: [
          new HtmlWebpackPlugin({
            title: 'Webpack Tutorial',
            template: './src/index.html'
          new webpack.HotModuleReplacementPlugin()
      if (env === 'production') {
        config.mode = 'production'
        config.devtool = false
        config.plugins = [
          ...config.plugins,
          new CleanWebpackPlugin(),
          new CopyWebpackPlugin(['public'])
      return config
    
    // 例如此为生产环境配置文件:webpack.prod.js 
    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',
      plugins: [
        new CleanWebpackPlugin(),
        new CopyWebpackPlugin(['public'])
    

    Webpack会在生产模式下自动开启Tree Shaking 来去除冗余代码,而在开发环境中,可在配置文件中使用:

    optimization: {
        // 模块只导出被使用的成员(标记无用代码)
        usedExports: true,
        // 压缩输出结果(删掉无用代码)
        minimize: true
        // 尽可能合并每一个模块到一个函数中
        concatenateModules: true
    

    Tree Shaking使用的前提是,由webpack打包的代码必须使用ESM;因此有可能与babel冲突失效,解决方法是babel-loader的配置选项指定modules。

    use: {
      loader: 'babel-loader',
      options: {
        presets: [
          // 如果 Babel 加载模块时已经转换了 ESM,则会导致 Tree Shaking 失效
          // ['@babel/preset-env', { modules: 'commonjs' }]
          // ['@babel/preset-env', { modules: false }]
          // 也可以使用默认配置,也就是 auto,这样 babel-loader 会自动关闭 ESM 转换
          ['@babel/preset-env', { modules: 'auto' }]
    

    动态导入(动态导入的模块会被自动分包)

    if (hash === '#posts') {
     import(/* webpackChunkName: 'components' */'./posts/posts').then(({ default: posts }) => {
        mainElement.appendChild(posts())
    } else if (hash === '#album') {
     import(/* webpackChunkName: 'components' */'./album/album').then(({ default: album }) => {
        mainElement.appendChild(album())
    

    Webpack 输出文件名 Hash模式有三种:hash、chunkhash、contenthash:

    output: {
        filename: '[name]-[contenthash:8].bundle.js'
    
    // rollup.config.js
    import json from 'rollup-plugin-json'
    import resolve from 'rollup-plugin-node-resolve'
    export default {
      input: 'src/index.js',
      output: {
        file: 'dist/bundle.js',
        format: 'iife'
      plugins: [
        json(),
        resolve()
    // 运行:yarn rollup --config
    

    Stylelint用于检查CSS代码格式,有cli工具,支持sass/less/postCss,支持Gulp/Webpack:

    // .stylelintrc.js
    module.exports = {
      extends: [
        'stylelint-config-standard',
        'stylelint-config-sass-guidelines'
    

    Husky可以实现Git Hooks的使用需求,如提交前的eslint校验,lint-staged可以实现Husky后的代码格式化,二者在package.json中进行配合和配置。

    第三阶段:Vue.js框架源码与进阶

    模块一:手写 Vue Router、手写响应式实现、虚拟 DOM 和 Diff 算法

    任务一:Vue.js 基础回顾

    vue基础结构:

      new Vue({
        data: {
          company: {
            name: '拉勾',
            address: '中关村创业大街籍海楼4层'
        render(h) {
          return h('div', [
            h('p', '公司名称:' + this.company.name),
            h('p', '公司地址:' + this.company.address)
      }).$mount('#app')
    
    const path = require('path')
    // 导入处理 history 模式的模块
    const history = require('connect-history-api-fallback')
    // 导入 express
    const express = require('express')
    const app = express()
    // 注册处理 history 模式的中间件
    app.use(history())
    // 处理静态资源的中间件,网站根目录 ../web
    app.use(express.static(path.join(__dirname, '../web')))
    // 开启服务器,端口是 3000
    app.listen(3000, () => {
      console.log('服务器开启,端口:3000')
        _Vue = Vue
        //3 把创建Vue的实例传入的router对象注入到Vue实例
        // _Vue.prototype.$router = this.$options.router
        _Vue.mixin({
          beforeCreate() {
            if (this.$options.router) {
              _Vue.prototype.$router = this.$options.router
      constructor(options) {
        this.options = options
        this.routeMap = {}
        // observable
        this.data = _Vue.observable({
          current: "/"
        this.init()
      init() {
        this.createRouteMap()
        this.initComponent(_Vue)
        this.initEvent()
      createRouteMap() {
        //遍历所有的路由规则 吧路由规则解析成键值对的形式存储到routeMap中
        this.options.routes.forEach(route => {
          this.routeMap[route.path] = route.component
      initComponent(Vue) {
        Vue.component("router-link", {
          props: {
            to: String
          render(h) {
            return h("a", {
              attrs: {
                href: this.to
              on: {
                click: this.clickhander
            }, [this.$slots.default])
          methods: {
            clickhander(e) {
              history.pushState({}, "", this.to)
              this.$router.data.current = this.to
              e.preventDefault()
          // template:"<a :href='to'><slot></slot><>"
        const self = this
        Vue.component("router-view", {
          render(h) {
            // self.data.current
            const cm = self.routeMap[self.data.current]
            return h(cm)
      initEvent() {
        window.addEventListener("popstate", () => {
          this.data.current = window.location.pathname
          Object.keys(data).forEach(key => {
            // 把 data 中的属性,转换成 vm 的 setter/setter
            Object.defineProperty(vm, key, {
              enumerable: true,
              configurable: true,
              get () {
                console.log('get: ', key, data[key])
                return data[key]
              set (newValue) {
                console.log('set: ', key, newValue)
                if (newValue === data[key]) {
                  return
                data[key] = newValue
                // 数据更改,更新 DOM 的值
                document.querySelector('#app').textContent = data[key]
          set (target, key, newValue) {
            console.log('set, key: ', key, newValue)
            if (target[key] === newValue) {
              return
            target[key] = newValue
            document.querySelector('#app').textContent = target[key]
          constructor () {
            // { 'click': [fn1, fn2], 'change': [fn] }
            this.subs = Object.create(null)
          // 注册事件
          $on (eventType, handler) {
            this.subs[eventType] = this.subs[eventType] || []
            this.subs[eventType].push(handler)
          // 触发事件
          $emit (eventType) {
            if (this.subs[eventType]) {
              this.subs[eventType].forEach(handler => {
                handler()
    // 1. 导入模块
    import style from 'snabbdom/modules/style'
    import eventlisteners from 'snabbdom/modules/eventlisteners'
    // 2. 注册模块
    // 参数:数组,模块
    // 返回值:patch函数,作用对比两个vnode的差异更新到真实DOM
    let patch = init([
      style,
      eventlisteners
    // 3. 使用 h() 函数的第二个参数传入模块需要的数据(对象)
    // 第一个参数:标签+选择器
    // 第二个参数:如果是字符串的话就是标签中的内容
    let vnode = h('div', {
      style: {
        backgroundColor: 'red'
      on: {
        click: eventHandler
      h('h1', 'Hello Snabbdom'),
      h('p', '这是p标签')
    function eventHandler () {
      console.log('点击我了')
    let app = document.querySelector('#app')
    // 第一个参数:可以是DOM元素,内部会把DOM元素转换成VNode
    // 第二个参数:VNode
    // 返回值:VNde
    let oldVnode = patch(app, vnode)
    vnode = h('div', 'hello')
    patch(oldVnode, vnode)
    

    (2) 首先会对新老节点数组的开始和结尾节点设置标记索引,遍历的过程中移动索引,有四种情况 :

  • 如果 oldStartVnode 和 newStartVnode 是 sameVnode (key 和 sel 相同) ,调用 patchVnode() 对比和更新节点,把旧开始和新开始索引往后移动 oldStartIdx++ / newStartIdx++;
  • 如果 oldEndVnode和 newEndVnode 是 sameVnode (key 和 sel 相同) ,调用 patchVnode() 对比和更新节点,把旧结束和新结束索引往前移动 oldEndIdx-- / newEndIdx--;
  • 如果 oldStartVnode / newEndVnode (旧开始节点 / 新结束节点) 相同,调用 patchVnode() 对比和更新节点,把 oldStartVnode 对应的 DOM 元素,移动到右边,更新索引;
  • 如果 oldEndVnode / newStartVnode (旧结束节点 / 新开始节点) 相同,调用 patchVnode() 对比和更新节点,把 oldEndVnode对应的 DOM 元素,移动到左边,更新索引;
  • (3) 如果不是以上四种情况:

  • 遍历新节点,使用 newStartNode 的 key 在老节点数组中找相同节点;
  • 如果没有找到,说明 newStartNode 是新节点,创建新节点对应的 DOM 元素,插入到 DOM 树中;
  • 如果找到了,判断新节点和找到的老节点的 sel 选择器是否相同,如果不相同,说明节点被修改了,重新创建对应的 DOM 元素,插入到 DOM 树中 ,如果相同,把 elmToMove 对应的 DOM 元素,移动到左边
  • 当老节点的所有子节点先遍历完 (oldStartIdx > oldEndIdx),循环结束;
  • 新节点的所有子节点先遍历完 (newStartIdx > newEndIdx),循环结束;
  • 如果老节点的数组先遍历完(oldStartIdx > oldEndIdx),说明新节点有剩余,把剩余节点批量插入到右边
  • 如果新节点的数组先遍历完(newStartIdx > newEndIdx),说明老节点有剩余,把剩余节点批量删除;
  • 不带 key 的情况需要进行两次 DOM 操作,带 key 的情况只需要更新一次 DOM 操作(移动 DOM 项),所以带 key 的情况可以减少 DOM 的操作。

    模块二: Vue.js 源码分析(响应式、虚拟 DOM、模板编译和组件化)

    任务一: Vue.js 源码剖析-响应式原理