Giter Site home page Giter Site logo

fe-weekly's People

Contributors

dailynodejs avatar happyworker101 avatar lin727 avatar malarkey-jhu avatar tangye1234 avatar tyz98 avatar ww4444444 avatar xinre avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

Forkers

xinre jaywongwz

fe-weekly's Issues

nuxt约定式路由实现

目录结构:
pages/
--| a.vue
--| b.vue
--| b/
-----| c.vue
-----| c/
-------| d.vue
生成的router:

  router: {
    routes: [
      {
        name: 'a',
        path: '/a',
        component: 'pages/a.vue'
      },
      {
        name: 'b',
        path: '/b',
        component: 'pages/b.vue',
        children: [
          {
            name: 'b-c',
            path: '/b/c',
            component: 'pages/b/c.vue',
            children: [
              {
                name: 'b-c-d',
                path: '/b/c/d',
                component: 'pages/b/c/d.vue',
              }
            ]
          },
        ]
      },
    ]
  }

@nuxt/builder/dist/builder.js
Builder的build方法

async build () {
  ...
  // Generate routes and interpret the template files
  await this.generateRoutesAndFiles();
  ...
}

generateRoutesAndFiles方法

async generateRoutesAndFiles () {
  ...
  await Promise.all([
    ...
    this.resolveRoutes(templateContext),
    ...
  ]);
  ...
}

resolveRoutes方法

async resolveRoutes ({ templateVars }) {
  ...
  else if (this._nuxtPages) {
    // Use nuxt.js createRoutes bases on pages/
    const files = {};
    const ext = new RegExp(`\\.(${this.supportedExtensions.join('|')})$`);
    for (const page of await this.resolveFiles(this.options.dir.pages)) {
      const key = page.replace(ext, '');
      // .vue file takes precedence over other extensions
      if (/\.vue$/.test(page) || !files[key]) {
        files[key] = page.replace(/(['"])/g, '\\$1');
      }
    }
    templateVars.router.routes = utils.createRoutes({
      files: Object.values(files),
      srcDir: this.options.srcDir,
      pagesDir: this.options.dir.pages,
      routeNameSplitter,
      supportedExtensions: this.supportedExtensions,
      trailingSlash
    });
  } 
  ...
}

resolveFiles使用glob库返回an array of filenames

@nuxt/utils/dist/utils.js

const createRoutes = function createRoutes ({
  files,
  srcDir,
  pagesDir = '',
  routeNameSplitter = '-',
  supportedExtensions = ['vue', 'js'],
  trailingSlash
}) {
  const routes = [];
  files.forEach((file) => {
    const keys = file
      .replace(new RegExp(`^${pagesDir}`), '')
      .replace(new RegExp(`\\.(${supportedExtensions.join('|')})$`), '')
      .replace(/\/{2,}/g, '/')
      .split('/')
      .slice(1);
    const route = { name: '', path: '', component: r(srcDir, file) };
    let parent = routes;
    keys.forEach((key, i) => {
      // remove underscore only, if its the prefix
      const sanitizedKey = key.startsWith('_') ? key.substr(1) : key;

      route.name = route.name
        ? route.name + routeNameSplitter + sanitizedKey
        : sanitizedKey;
      route.name += key === '_' ? 'all' : '';
      route.chunkName = file.replace(new RegExp(`\\.(${supportedExtensions.join('|')})$`), '');
      const child = parent.find(parentRoute => parentRoute.name === route.name);

      if (child) {
        child.children = child.children || [];
        parent = child.children;
        route.path = '';
      } else if (key === 'index' && i + 1 === keys.length) {
        route.path += i > 0 ? '' : '/';
      } else {
        route.path += '/' + getRoutePathExtension(key);

        if (key.startsWith('_') && key.length > 1) {
          route.path += '?';
        }
      }
    });
    if (trailingSlash !== undefined) {
      route.pathToRegexpOptions = { ...route.pathToRegexpOptions, strict: true };
      route.path = route.path.replace(/\/+$/, '') + (trailingSlash ? '/' : '') || '/';
    }

    parent.push(route);
  });

  sortRoutes(routes);
  return cleanChildrenRoutes(routes, false, routeNameSplitter)
};

举例说明createRoutes方法:

files: 
[
  'pages/a.vue',
  'pages/b.vue',
 ' pages/b/c.vue',
 ' pages/b/c/d.vue'
]
//初始化routes为[]
routes: []

遍历files,每个file最终会得到一个route对象,并且会按照嵌套关系放在正确的“parent”中(可能是routes列表本身,或者某一个route的children列表)
 每个file生成一个keys列表,keys即每个.vue文件的“路径”, 之后在遍历keys的过程中要更新route对象并更新最后要插入其中的parent对象。

 遍历keys前初始化route为一个name、path均为空的对象,parent为最外层的routes:
route = { name: '', path: '', component: r(srcDir, file) }; parent = routes

后面的例子中route中省略path和component

  file:"pages/a.vue"
  keys: ["a"]
  route = { name: '' };
  parent = routes
  //开始遍历keys
    key: "a"
    route = { name: 'a' };
    //在parent中检索与route.name同名的route对象,若有则更新parent
    child = undefined
    //parent不变
  //keys遍历结束,把route对象push到最终的parent中
  routes: [{ name: 'a' }]
  file:"pages/b.vue"
  keys: ["b"]
  route = { name: '' };
  parent = routes
  //开始遍历keys
    key: "b"
    route = { name: 'b' };
    //在parent中检索与route.name同名的route对象,若有则更新parent
    child = undefined
    //parent不变
  //keys遍历结束,把route对象push到最终的parent中
  routes: [{ name: 'a' }, { name: 'b' }]
  file:"pages/b/c.vue"
  keys: ["b", "c"]
  route = { name: '' };
  parent = routes
  //开始遍历keys
    key: "b"
    route = { name: 'b' };
    //在parent中检索与route.name同名的route对象,若有则更新parent
    //找到child但没有children属性,给child增加children属性并初始化为[]
    child = { name: 'b', children: [] }
    parent =  []

    key: "c"
    route = { name: 'b-c' };
    //在parent中检索与route.name同名的route对象,若有则更新parent
    child = undefined
    //parent不变

  //keys遍历结束,把route对象push到最终的parent中
  {name: 'b', children: [{ name: 'b-c' }]}
  //此时routes为:
  routes: [ { name: 'a' }, { name: 'b', children: [{ name: 'b-c' }] }]
  file:"pages/b/c/d.vue"
  keys: ["b", "c", "d"]
  route = { name: '' };
  parent = routes
  //开始遍历keys
    key: "b"
    route = { name: 'b' };
    //在parent中检索与route.name同名的route对象,若有则更新parent
    child = { name: 'b', children: [{ name: 'b-c' }] }
    parent = [{ name: 'b-c' }]

    key: "c"
    route = { name: 'b-c' };
    //在parent中检索与route.name同名的route对象,若有则更新parent
    //找到child但没有children属性,给child增加children属性并初始化为[]
    child = { name: 'b-c', chilren: [] }
    parent = []

    key: "d"
    route = { name: 'b-c-d' };
    //在parent中检索与route.name同名的route对象,若有则更新parent
    //找到child但没有children属性,给child增加children属性并初始化为[]
    child = undefined
    //parent不变

  //keys遍历结束,把route对象push到最终的parent中
  { name: 'b-c', chilren: [{ name: 'b-c-d' }] }
  //此时routes为:
  routes: [{ name: 'a' }, { name: 'b', children: [{ name: 'b-c', chilren: [{ name: 'b-c-d' }] }] }]

所以最终生成的routes为[{ name: 'a' }, { name: 'b', children: [{ name: 'b-c', chilren: [{ name: 'b-c-d' }] }] }]

React 原理

requestIdleCallback

在浏览器一帧的剩余空闲时间内执行优先度相对较低的任务,一般按先进先调用的顺序执行

语法

var handle = window.requestIdleCallback(callback[, options])

缺陷

is called only 20 times per second - Chrome on my 6x2 core Linux machine, it's not really useful for UI work。—— from Releasing Suspense

与 requestAnimationFrame 区别?

requestAnimationFrame 回调会在每一帧确定执行,属于高优先级任务。

如图:Life of a frame

可以执行 DOM 修改吗?

不建议,在 requestAnimationFrame 里面进行

Promise resolve 可以放到里面吗?

不建议

兼容版本源码

地址:https://github.com/santiagogil/request-idle-callback/blob/master/index.js

var requestIdleCallback = function (cb) {
    if (global.requestIdleCallback) return global.requestIdleCallback(cb)
    var start = Date.now();
    return setTimeout(function () {
      cb({
        didTimeout: false,
        timeRemaining: function () {
          return Math.max(0, 50 - (Date.now() - start));
        }
      });
    }, 1);
  }

var cancelIdleCallback = function (id) {
  if (global.cancelIdleCallback) return global.cancelIdleCallback(id)
   return clearTimeout(id);
}

exports.requestIdleCallback = requestIdleCallback
exports.cancelIdleCallback = cancelIdleCallback

参考链接

element-UI checkbox组件value,checked属性

element-UI checkbox组件value,checked属性

只考虑单独一个el-checkbox的情况,不考虑其在el-checkbox-group中的情况

template中的input(只保留相关部分)

<input
  type="checkbox"
  :value="label"
  v-model="model"
  @change="handleChange">

input的type为checkbox时,value指的是checkbox选中时提交表单的值(默认为on),不控制表单的选中状态。input checkbox的v-model使用 checked property 和 change 事件。

props: checked,value

注意组件props中的checked和value并不对应input的checked和value属性

组件初始化时的状态

created:

created() {
  this.checked && this.addToStore();
},
addToStore() {
  if (
    Array.isArray(this.model) &&
    this.model.indexOf(this.label) === -1
  ) {
    this.model.push(this.label);
  } else {
    this.model = this.trueLabel || true;
  }
},

model计算属性(省略在el-checkbox-group中的情况)

computed: {
  model: {
    get() {
      return this.value !== undefined ? this.value : this.selfModel;
    },
    set(val) {
      this.$emit('input', val);
      this.selfModel = val;
    }
  }
},

created时,若checked为true, 则会set计算属性model, set方法中更新selfModel。input标签中v-model="model", 在不传value时model的get方法取的是selfModel, 在传value属性时取的就是value本身。所以,若传入checked为true不传value时,初始化时checkbox会勾选;若传了value属性, checked就会失去作用, value为true就初始化勾选。

created后不论如何修改checked,只要不重新创建组件,都不会有作用。

用户操作checkbox时的状态

input的change事件回调handleChange(省略checkbox-group和指定trueLabel,falseLabel时的逻辑)

handleChange(ev) {
  let value = ev.target.checked;
  this.$emit('change', value, ev);
}

用户操作checkbox时,会emit一个change事件。

注意上面emit的change是组件上的change,而本身触发这个回调的change是用户触发的input上的change,这个change本身会触发input自己的v-model,也就是model的set方法,在set方法中还emit了"input"事件,并更新selfModel。

所以,用户操作会emit两个事件,"input"和"change", 并更新selfModel。

如果在组件上用了v-model而不仅仅是value,比如传入v-model="checkboxChecked",就会由于input事件而更新外部的checkboxChecked(一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件)

如果在组件上仅仅使用了value="checkboxChecked",那么将不会更新外部的checkboxChecked。

修改value prop后的状态

修改value prop会使得model这个计算属性get得到的数据变化,input checkbox的v-model="model", 从而改变input的checked状态

修改checked prop后的状态

created后不论如何修改checked,只要不重新创建组件,都不会有作用。

社区播放器详情页兼容低版本浏览器

uncaught SyntaxError:Unexpected identifier

出现了 let 定义变量

let 定义变量

query-string 的使用

This module targets Node.js 6 or later and the latest version of Chrome, Firefox, and Safari. If you want support for older browsers, or, if your project is using create-react-app v1, use version 5: npm install query-string@5.

地址:query-string

nuxt 配置里面的 extendRoutes

配置

extendRoutes (routes) {
    return [
        ...routes,
        {
          name: 'about-bis',
          path: '/about-bis',
          component: '~/pages/about.vue',
          meta: { text: 'test-meta' }
        },
        {
          path: '/redirect/about-bis',
          redirect: '/about-bis'
        },
        {
          path: '/not-existed'
        }
    ]
}

resolveRoutes 函数

packages/builder/src/builder.js

async resolveRoutes ({ templateVars }) {
  if (typeof this.options.router.extendRoutes === 'function') {
      const extendedRoutes = await this.options.router.extendRoutes(
        templateVars.router.routes,
        r
      )

      if (extendedRoutes !== undefined) {
        templateVars.router.routes = extendedRoutes
      }
  }
}

pm2 相关

脚本文件

官方配置项地址说明:如下

cron_restart

string 类型,比如 “1 0 * * *”

a cron pattern to restart your app. Application must be running for cron feature to work

max_memory_restart

string 类型,比如 “150M”

your app will be restarted if it exceeds the amount of memory specified. human-friendly format : it can be “10M”, “100K”, “2G” and so on…

动态路由相关

第一个问题,路径里面有 [] 的如何处理

先判断,isDynamicRoute 源码地址

const TEST_ROUTE = /\/\[[^/]+?\](?=\/|$)/

export function isDynamicRoute(route: string): boolean {
  return TEST_ROUTE.test(route)
}

element-UI form,form-item组件中validate的实现

element-UI form,form-item组件中validate相关源码

核心:form-item的validate方法 (使用了async-validator)

validate(trigger, callback = noop)

trigger: string || 空

callback接受两个参数(message, invalidFields)
//作用:找出所有trigger包含指定trigger的rules,作为descriptor对this.fieldValue进行验证,验证完毕更新validateState和validateMessage,并调用指定的回调函数callback(this.validateMessage, invalidFields)

注意使用的validator.validate中传入了option为{ firstFields: true }, errors中只有一个错误

async-validator validate方法API文档

import AsyncValidator from 'async-validator';//引入async-validator

validate(trigger, callback = noop) {
  this.validateDisabled = false;//?
  const rules = this.getFilteredRule(trigger);//取出所有可被触发的rules数组
  if ((!rules || rules.length === 0) && this.required === undefined) {
    callback();
    return true;
  }
  this.validateState = 'validating';//开始验证,validateState为'validating'
  const descriptor = {};
  if (rules && rules.length > 0) {
    rules.forEach(rule => {
      delete rule.trigger;//async-validator的descriptor中没有trigger这个属性,所以删除
    });
  }
  descriptor[this.prop] = rules;//传入async-validator构造函数的descriptor
  const validator = new AsyncValidator(descriptor);//AsyncValidator
  const model = {};
  model[this.prop] = this.fieldValue;//传入async-validator的validate方法的model(待validate的对象)
  //validate方法文档见代码下方
  validator.validate(model, { firstFields: true }, (errors, invalidFields) => {//验证完毕的回调:errors is an array of all errors, fields is an object keyed by field name with an array of errors per field
    this.validateState = !errors ? 'success' : 'error';//validate完毕更新validateState为'success'或'error'
    this.validateMessage = errors ? errors[0].message : '';//validate完毕更新validateMessage为所有errors中的第一个error的message(error构造函数中传入的字符串就是这个error对象的.message)
    callback(this.validateMessage, invalidFields);//验证完毕调用this.validate方法传入的callback(错误message,错误的fields[{name: [error,,,]}])
    this.elForm && this.elForm.$emit('validate', this.prop, !errors, this.validateMessage || null);//elForm emit一个validate事件,参数为validate完成的是哪一个prop, 是否正确, 验证message(为什么这里用了inject的elForm没有用form???)
  });
},

哪里在调用validate

1. form-item的onFieldBlur和onFieldChange

onFieldBlur() {
  this.validate('blur');//调用this.validate
},
onFieldChange() {
  if (this.validateDisabled) {
    this.validateDisabled = false;
    return;
  }
  this.validate('change');//调用this.validate
},
addValidateEvents() {
  const rules = this.getRules();
  if (rules.length || this.required !== undefined) {
    this.$on('el.form.blur', this.onFieldBlur);//el.form.blur时触发this.onFieldBlur
    this.$on('el.form.change', this.onFieldChange);//el.form.blur时触发this.onFieldChange
  }
},
removeValidateEvents() {//父form的rules改变时调用,先removeValidateEvents再addValidateEvents
  this.$off();//解除'el.form.blur'和'el.form.change'事件的监听
}

2. form的validateField

validateField(props, cb)

依次调用props对应的form-item的validate方法,cb传入每次validate的callback(仍是接受两个参数(message, invalidFields))

form-item的validate方法第一个参数trigger传入'',即不论rules中的trigger为何值都触发验证

validateField(props, cb) {
  props = [].concat(props);
  const fields = this.fields.filter(field => props.indexOf(field.prop) !== -1);
  if (!fields.length) {
    console.warn('[Element Warn]please pass correct props!');
    return;
  }//filter出props对应的fields(form-item数组)
  fields.forEach(field => {//依次调用form-item的validate方法
    field.validate('', cb);
  });
},

3. form的validate

validate(callback)

callback为function时接受两个参数(valid, invalidFields) valid为boolean验证通过为true, invalidFields为array [{name,[error,]},]

callback若为空或不为function,返回一个promise,验证结束时promise状态改变

validate(callback) {
  if (!this.model) {
    console.warn('[Element Warn][Form]model is required for validate to work!');
    return;
  }
  let promise;
  // if no callback, return promise
  if (typeof callback !== 'function' && window.Promise) {
    promise = new window.Promise((resolve, reject) => {
      callback = function(valid) {
        valid ? resolve(valid) : reject(valid);
      };
    });
  }
  let valid = true;
  let count = 0;
  // 如果需要验证的fields为空,调用验证时立刻返回callback
  if (this.fields.length === 0 && callback) {
    callback(true);
  }
  let invalidFields = {};
  this.fields.forEach(field => {
    field.validate('', (message, field) => {
      if (message) {
        valid = false;
      }
      invalidFields = objectAssign({}, invalidFields, field);//把所有form-item的错误汇总到invalidFields中
      if (typeof callback === 'function' && ++count === this.fields.length) {//如果所有form-item都验证完毕且callback是function,则调用callback
      //注:本身传入的callback不是function时,在上面promise的executor中也把callback变成了function,这个callback可以改变所返回promise的状态,若valid=true则promise resolve,否则reject
        callback(valid, invalidFields);
      }
    });
  });
  if (promise) {//如果传入的callback不是function,返回promise,验证完毕时promise的状态会改变
    return promise;
  }
},

form与form-item生命周期钩子

关系到form-item在form中的添加和删除

form created

form创建,监听两个事件'el.form.addField'和'el.form.removeField',管理this.fields

created() {
  this.$on('el.form.addField', (field) => {//子form-item mounted时,把form-item实例添加进this.fields
    if (field) {
      this.fields.push(field);
    }
  });
  /* istanbul ignore next */
  this.$on('el.form.removeField', (field) => {//子form-item beforeDestroyed时,把form-item实例从this.fields中删除
    if (field.prop) {//这个判断对应form-item mounted中的if(this.prop), 只有有prop的item才添加,自然也只有有prop的item才需要删除
      this.fields.splice(this.fields.indexOf(field), 1);
    }
  });
},

form-item mounted

form-item mounted时触发'el.form.addField',并记录此form-item的初始值(resetField时需要用),且为此form-item添加'el.form.blur'和'el.form.change'监听,回调为validate方法

mounted() {
  if (this.prop) {
    this.dispatch('ElForm', 'el.form.addField', [this]);////form-item mounted时触发el.form.addField事件,此时form把本form-item实例添加到form.fields中(只有有prop属性时才传入触发事件通知form)
    let initialValue = this.fieldValue;
    if (Array.isArray(initialValue)) {
      initialValue = [].concat(initialValue);
    }
    Object.defineProperty(this, 'initialValue', {
      value: initialValue
    });//form-item mounted时还记录此form-item的初始值(resetField时需要用)
    this.addValidateEvents();//form-item mounted时添加'el.form.blur'和'el.form.change'监听,回调为validate方法(具体见form-item validate方法 ‘哪里在调用validate’)
  }
},

form-item mounted

form-item销毁时触发'el.form.removeField'

beforeDestroy() {
  this.dispatch('ElForm', 'el.form.removeField', [this]);
}

上面涉及到的关键method/computed

form (computed)

找到这个form-item的父form(form-item中可能嵌套form-item)

form() {
  let parent = this.$parent;
  let parentName = parent.$options.componentName;
  while (parentName !== 'ElForm') {
    if (parentName === 'ElFormItem') {
      this.isNested = true;
    }
    parent = parent.$parent;
    parentName = parent.$options.componentName;
  }
  return parent;
},

fieldValue (computed)

返回父form.model[this.prop]

fieldValue() {
  const model = this.form.model;//model是父form的prop
  if (!model || !this.prop) { return; }
  let path = this.prop;
  if (path.indexOf(':') !== -1) {
    path = path.replace(/:/, '.');
  }
  return getPropByPath(model, path, true).v;//getPropByPath方法(在element的utils中,用于以字符串的形式寻找相应属性)
},

getRules

返回rules数组(如果这个form-item本身传入了rules prop,则返回的是rules+本身传入的required prop;如果本身未传入rules prop,则返回的是父form中提取处的本item的rules+本身传入的required prop)

所以若form-item本身传入了rules prop,则忽略form中与本item有关的prop

getRules() {
  let formRules = this.form.rules;//this.form.rules是父form中传入的prop
  const selfRules = this.rules;//this.rules是本form-item传入的prop (rules: [Object, Array])????
  const requiredRule = this.required !== undefined ? { required: !!this.required } : [];
  const prop = getPropByPath(formRules, this.prop || '');
  formRules = formRules ? (prop.o[this.prop || ''] || prop.v) : [];//getPropByPath方法
  return [].concat(selfRules || formRules || []).concat(requiredRule);
},

getFilteredRule

返回可以触发验证的rules数组(所有rules中,trigger包含指定trigger的rule和没明确trigger的rule)

getFilteredRule(trigger) {
  const rules = this.getRules();
  return rules.filter(rule => {
    if (!rule.trigger || trigger === '') return true;//rule中没明确trigger时,任何trigger都可以触发
    if (Array.isArray(rule.trigger)) {
      return rule.trigger.indexOf(trigger) > -1;
    } else {
      return rule.trigger === trigger;
    }
  }).map(rule => objectAssign({}, rule));
},

babel polyfill, runtime 对ES6新API的转译

babel polyfill, runtime 对ES6新API的转译

明确preset-env, polyfill, runtime作用范围

  • 如果在presets中设置preset-env,不做其他设置,则只会转译ES6的新语法(比如箭头函数等),不会转译ES6的API,比如Promise
  • 如果还要转译ES6的API,则需要搭配@babel/polyfill or @babel/runtime-corejs2 + @babel/plugin-transform-runtime

@babel/polyfill 与 @babel/runtime-corejs2

  • 相同:
    @babel/polyfill 和@babel/runtime-corejs2 都使用了 core-js(v2)这个库来进行 api 的处理。
  • 不同:
    core-js(v2)这个库有两个核心的文件夹,分别是 library 和 modules。@babel/runtime-corejs2 使用 library 这个文件夹,@babel/polyfill 使用 modules 这个文件夹。
    • library 使用 helper 的方式,局部实现某个 api,不会污染全局变量
    • modules 以污染全局变量的方法来实现 api
      (这也是runtime无法处理原型上的api的原因,因为要模拟这些api,必须要污染全局变量。)

@babel/plugin-transform-runtime , @babel/runtime, @babel/runtime-corejs2, @babel/runtime-corejs3

  • 背景: babel在转码过程中,会加入很多babel自己的helper函数,这些helper函数,在每个文件里可能都会重复存在。
  • transform-runtime插件可以把这些重复的helper函数,转换成公共的、单独的依赖引入,从而节省转码后的文件大小;是一个开发环境的dependency
  • 在transform-runtime作用的过程中,都会使用@babel/runtime内部的模块,来代替前面讲到的重复的helper函数,在runtime时就会引用这些模块;是一个生产环境的dependency。
  • 默认情况下,transform-runtime是不启用对core-js的polyfill处理的,所以安装@babel/runtime就够了。 但是如果想启用transform-runtime对core-js的polyfill的话,就得使用@babel/runtime另外的两个版本。 core-js@2对应的@babel/runtime版本是:@babel/runtime-corejs2;core-js@3对应的@babel/runtime版本是:@babel/runtime-corejs3。所以根据是否启用core-js的polyfill,以及core-js的版本,实际使用babel的runtime,有三种安装类型:
    # disable core-js polyfill
    npm install --save-dev @babel/plugin-transform-runtime
    npm install --save @babel/runtime
    
    # enable core-js@2 polyfill
    npm install --save-dev @babel/plugin-transform-runtime
    npm install --save @babel/runtime-corejs2
    
    # enable core-js@3 polyfill
    npm install --save-dev @babel/plugin-transform-runtime
    npm install --save @babel/runtime-corejs3
    

转译ES6的API + ES6的API的几种方式

  1. 方式一 preset-env + @babel/polyfill

    设置preset-env的options.useBuiltIns(false | 'entry' | 'usage' 默认为false)

    • 设置为false时,会把@babel/polyfill这个包引进来,忽略targets配置项和项目里的browserslist配置。
      需要在webpack的入口文件里import "@babel/polyfill"
    • 设置为entry时,在整个项目里,只需要引入一次@babel/polyfill,它会根据targets和browserslist,然后只加载目标环境不支持的api文件
      需要在webpack的入口文件里import "@babel/polyfill"
    • 设置为usage时,babel会在目标环境的基础上,只加载项目里用的那些不支持的api的文件,做到按需加载
      自动检测,不需手动import @babel/polyfill(但仍需安装)

    所以,这种方式的配置为:
    安装@babel/preset-env和@babel/polyfill
    入口文件(useBuiltIns使用false或entry时)

    import "@babel/polyfill"

    babel配置文件

    {
        "presets": [
          [
            "@babel/preset-env",
            {
              "useBuiltIns": "entry" //(|  "usage" | false )
            }
          ]
        ]
    }
  2. 方式二 preset-env + @babel/plugin-transform-runtime + @babel/runtime-corejs2
    preset-env不需设置useBuiltIns, 但需要设置@babel/plugin-transform-runtime的options.corejs(boolean or number,默认值是false)

    • 设置为false时,只对语法进行转换,不对api进行处理(需安装@babel/runtime)
    • 设置为2时,会对api进行处理,但不能polyfill Array.includes这种需要重写property的API(需安装@babel/runtime-corejs2)
    • 设置为3时,见下面corejs升级到v3之后的变化--方式二的变化

    所以,这种方式的配置为:
    安装@babel/preset-env, @babel/runtime-corejs2和@babel/plugin-transform-runtime
    入口文件不需做任何引入
    babel配置文件

    {
      "presets": ["@babel/preset-env"],
      "plugins": [
        [
          "@babel/plugin-transform-runtime",
          {
            "corejs": 2
          }
        ]
      ]
    }

corejs升级到v3之后的变化

support instance methods!!
corejs3

  1. 上面方式一的变化
    @babel/polyfill 无法提供 core-js@2 向 core-js@3 过渡,所以使用preset-env + @babel/polyfill的方式无法提供corejs3的新特性。由于@babel/polyfill实际上是regenerator runtime和core-js两个库的结合,所以如果要使用corejs@3,可以不安装和引入@babel/polyfill,而安装和引入上面两个库。且要在preset-env的options中设置corejs: 3(corejs:2, 3 or { version: 2 | 3, proposals: boolean }, defaults to 2.)
    所以,这种方式的配置为:
    安装@babel/preset-env, core-js, regenerator-runtime
    入口文件(useBuiltIns使用false或entry时)
    import "core-js/stable";
    import "regenerator-runtime/runtime";
    babel配置文件
    {
        "presets": [
          [
            "@babel/preset-env",
            {
              "useBuiltIns": "entry" //(|  "usage" | false )
              "corejs": {
                  "version": 3, // 使用core-js@3
                  "proposals": true,
               }
            }
          ]
        ]
    }
  2. 上面方式二的变化
    设置@babel/plugin-transform-runtime的options.corejs为3

参考:

async-validator 探究

代码地址:https://github.com/yiminghe/async-validator
有一个网络的中文翻译版本:https://www.cnblogs.com/wozho/p/10955525.html

如何使用

第一步:导入包

import Schema from 'async-validator'

第二步:new Schema 实例化

传入 descriptor 是一个对象

const descriptor = {
  // ...
}
const validator = new Schema(descriptor)

descriptor 对象

简单示例:

定义一个 keyname,值是一个对象

const descriptor = {
  name: { type: 'string', required: true }
}

descriptor 对象的 value

type

官方支持:

  • string: Must be of type string. This is the default type.
  • number: Must be of type number.
  • boolean: Must be of type boolean.
  • method: Must be of type function.
  • regexp: Must be an instance of RegExp or a string that does not generate an exception when creating a new RegExp.
  • integer: Must be of type number and an integer.
  • float: Must be of type number and a floating point number.
  • array: Must be an array as determined by Array.isArray.
  • object: Must be of type object and not Array.isArray.
  • enum: Value must exist in the enum.
  • date: Value must be valid as determined by Date
  • url: Must be of type url.
  • hex: Must be of type hex.
  • email: Must be of type email.
  • any: Can be any type.

1、type 设置 object

const descriptor = {
  address: {
    type: 'object',
    required: true,
    fields: {
      street: { type: 'string', required: true },
      city: { type: 'string', required: true },
      zip: { type: 'string', required: true, len: 8, message: 'invalid zip' },
    },
  }
}

底层 validator 之 object

validator/object.js

import rules from '../rule/index.js';
import { isEmptyValue } from '../util';

function object(rule, value, callback, source, options) {
  const errors = [];
  const validate =
    rule.required || (!rule.required && source.hasOwnProperty(rule.field));
  if (validate) {
    if (isEmptyValue(value) && !rule.required) {
      return callback();
    }
    rules.required(rule, value, source, errors, options);
    if (value !== undefined) {
      rules.type(rule, value, source, errors, options);
    }
  }
  callback(errors);
}

export default object;

2、type 设置 array

const descriptor = {
  roles: {
    type: 'array',
    required: true,
    len: 3,
    fields: {
      0: { type: 'string', required: true },
      1: { type: 'string', required: true },
      2: { type: 'string', required: true },
    },
  },
}

底层 validator 之 array

validator/array.js

import rules from '../rule/index';
function array(rule, value, callback, source, options) {
  const errors = [];
  const validate =
    rule.required || (!rule.required && source.hasOwnProperty(rule.field));
  if (validate) {
    if ((value === undefined || value === null) && !rule.required) {
      return callback();
    }
    rules.required(rule, value, source, errors, options, 'array');
    if (value !== undefined && value !== null) {
      rules.type(rule, value, source, errors, options);
      rules.range(rule, value, source, errors, options);
    }
  }
  callback(errors);
}

export default array;

3、type 设置 enum

const descriptor = {
  role: { type: 'enum', enum: ['admin', 'user', 'guest'] },
}

底层 validator 之 enum

validator/enum.js

import rules from '../rule/index.js';
import { isEmptyValue } from '../util';

const ENUM = 'enum';
function enumerable(rule, value, callback, source, options) {
  const errors = [];
  const validate =
    rule.required || (!rule.required && source.hasOwnProperty(rule.field));
  if (validate) {
    if (isEmptyValue(value) && !rule.required) {
      return callback();
    }
    rules.required(rule, value, source, errors, options);
    if (value !== undefined) {
      rules[ENUM](rule, value, source, errors, options);
    }
  }
  callback(errors);
}

export default enumerable;

4、type 设置 string

const descriptor = {
  name: { type: 'string', required: true }
}

底层 validator 之 string

import rules from '../rule/index.js';
import { isEmptyValue } from '../util';

function string(rule, value, callback, source, options) {
  const errors = [];
  const validate =
    rule.required || (!rule.required && source.hasOwnProperty(rule.field));
  if (validate) {
    if (isEmptyValue(value, 'string') && !rule.required) {
      return callback();
    }
    rules.required(rule, value, source, errors, options, 'string');
    if (!isEmptyValue(value, 'string')) {
      rules.type(rule, value, source, errors, options);
      rules.range(rule, value, source, errors, options);
      rules.pattern(rule, value, source, errors, options);
      if (rule.whitespace === true) {
        rules.whitespace(rule, value, source, errors, options);
      }
    }
  }
  callback(errors);
}

export default string;

5、type 设置 any

const descriptor = {
  name: { type: 'any' }
}

6、type 设置 number

const descriptor = {
  age: {
    type: 'number',
    asyncValidator: (rule, value) => {
      return new Promise((resolve, reject) => {
        if (value < 18) {
          reject('too young');  // reject with error message
        } else {
          resolve();
        }
      });
    },
  }
}

validator/number.js

import rules from '../rule/index.js';
import { isEmptyValue } from '../util';

function number(rule, value, callback, source, options) {
  const errors = [];
  const validate =
    rule.required || (!rule.required && source.hasOwnProperty(rule.field));
  if (validate) {
    if (value === '') {
      value = undefined;
    }
    if (isEmptyValue(value) && !rule.required) {
      return callback();
    }
    rules.required(rule, value, source, errors, options);
    if (value !== undefined) {
      rules.type(rule, value, source, errors, options);
      rules.range(rule, value, source, errors, options);
    }
  }
  callback(errors);
}

export default number;
required

The required rule property indicates that the field must exist on the source object being validated.

底层 rule 之 required

import * as util from '../util'
function required(rule, value, source, errors, options, type) {
  if (
    rule.required &&
    (!source.hasOwnProperty(rule.field) ||
      util.isEmptyValue(value, type || rule.type))
  ) {
    errors.push(util.format(options.messages.required, rule.fullField));
  }
}

export default required
defaultField
validator

You can custom validate function for specified field:

const descriptor = {
  field2: {
    validator(rule, value, callback) {
      return new Error(`${value} is not equal to 'test'.`);
    },
  }
}
pattern

The pattern rule property indicates a regular expression that the value must match to pass validation.

内置的 pattern

const descriptor = {
  email: { type: 'string', required: true, pattern: Schema.pattern.email }
}

自己写的 pattern /^[a-z]+$/

const descriptor = {
  name: {
    type: 'string',
    required: true,
    pattern: /^[a-z]+$/,
    transform(value) {
      return value.trim();
    },
  }
}
asyncValidator
message
{ 
  name: {
    type: 'string', 
    required: true, 
    message: 'Name is required' 
  } 
}

源码分析

文件目录

src
-- rule 文件夹
---- index.js (入口,负责导出目录内的文件方法)
---- enum.js
---- pattern.js
---- range.js
---- required.js
---- type.js
---- whitespace.js

-- validate 文件夹
---- index.js (入口,负责导出目录内的文件方法)
---- any.js
---- array.js
---- boolean.js
---- date.js
---- enum.js
---- float.js
---- integer.js
---- method.js
---- number.js
---- object.js
---- pattern.js
---- regexp.js
---- required.js
---- string.js
---- type.js

-- index.js

rule/index.js

import required from './required';
import whitespace from './whitespace';
import type from './type';
import range from './range';
import enumRule from './enum';
import pattern from './pattern';

export default {
  required,
  whitespace,
  type,
  range,
  enum: enumRule,
  pattern,
}

处理 type 的 type.js

rule/type.js

先判断 required

import required from './required';

function type(rule, value, source, errors, options) {
  if (rule.required && value === undefined) {
    required(rule, value, source, errors, options);
    return;
  }
}

export default type

内置的 type

const types = {
  integer(value) {
    return types.number(value) && parseInt(value, 10) === value;
  },
  float(value) {
    return types.number(value) && !types.integer(value);
  },
  array(value) {
    return Array.isArray(value);
  },
  regexp(value) {
    if (value instanceof RegExp) {
      return true;
    }
    try {
      return !!new RegExp(value);
    } catch (e) {
      return false;
    }
  },
  date(value) {
    return (
      typeof value.getTime === 'function' &&
      typeof value.getMonth === 'function' &&
      typeof value.getYear === 'function' &&
      !isNaN(value.getTime())
    );
  },
  number(value) {
    if (isNaN(value)) {
      return false;
    }
    return typeof value === 'number';
  },
  object(value) {
    return typeof value === 'object' && !types.array(value);
  },
  method(value) {
    return typeof value === 'function';
  },
  email(value) {
    return (
      typeof value === 'string' &&
      !!value.match(pattern.email) &&
      value.length < 255
    );
  },
  url(value) {
    return typeof value === 'string' && !!value.match(pattern.url);
  },
  hex(value) {
    return typeof value === 'string' && !!value.match(pattern.hex);
  },
}

获取当前配置的 type

function type(rule, value, source, errors, options) {
  const ruleType = rule.type;

  const custom = [
    'integer',
    'float',
    'array',
    'regexp',
    'object',
    'method',
    'email',
    'number',
    'date',
    'url',
    'hex',
  ];

  if (custom.indexOf(ruleType) > -1) {
    if (!types[ruleType](value)) {
      errors.push(
        util.format(
          options.messages.types[ruleType],
          rule.fullField,
          rule.type,
        ),
      );
    }
    // straight typeof check
  } else if (ruleType && typeof value !== rule.type) {
    errors.push(
      util.format(options.messages.types[ruleType], rule.fullField, rule.type),
    );
  }

}

validate/index.js

import string from './string';
import method from './method';
import number from './number';
import boolean from './boolean';
import regexp from './regexp';
import integer from './integer';
import float from './float';
import array from './array';
import object from './object';
import enumValidator from './enum';
import pattern from './pattern';
import date from './date';
import required from './required';
import type from './type';
import any from './any';

export default {
  string,
  method,
  number,
  boolean,
  regexp,
  integer,
  float,
  array,
  object,
  enum: enumValidator,
  pattern,
  date,
  url: type,
  hex: type,
  email: type,
  required,
  any,
};

Schema

函数 Schema 接收形参 descriptor

function Schema(descriptor) {
  this.rules = null;
  this._messages = defaultMessages;
  this.define(descriptor);
}

Schema.prototype = {
  messages(messages) {},
  define(rules) {},
  validate(source_, o = {}, oc = () => {}) {},
  getType(rule) {},
  getValidationMethod(rule) {}
}

export default Schema

核心方法 validate

核心方法 messages

自定义错误信息

const cn = {
  required: '%s 必填',
};
const descriptor = { name: { type: 'string', required: true } };
const validator = new Schema(descriptor);
validator.messages(cn);

工具方法

isEmptyObject

export function isEmptyObject(obj) {
  return Object.keys(obj).length === 0;
}

isEmptyValue

依赖内部方法 isNativeStringType

function isNativeStringType(type) {
  return (
    type === 'string' ||
    type === 'url' ||
    type === 'hex' ||
    type === 'email' ||
    type === 'date' ||
    type === 'pattern'
  );
}

export function isEmptyValue(value, type) {
  if (value === undefined || value === null) {
    return true;
  }
  if (type === 'array' && Array.isArray(value) && !value.length) {
    return true;
  }
  if (isNativeStringType(type) && typeof value === 'string' && !value) {
    return true;
  }
  return false;
}

getServerSideProps 里面哪来的 params?

参数用例

目录结构

pages
    ssr
         [id]
              index.tsx 

代码示例:

export default function Index () {
    return <>
      <p>[id] 目录路由作为动态参数</p>
    </>
}

export async function getServerSideProps(context: any) {
    // console.log('context', context)
    return {
        props: {}
    }
}

用户访问

浏览器访问:*****/ssr/server-side-rendered2

GetServerSidePropsContext 类型定义

源码地址

import { ParsedUrlQuery } from 'querystring'

export type GetServerSidePropsContext<Q extends ParsedUrlQuery = ParsedUrlQuery> = {
  req: IncomingMessage
  res: ServerResponse
  params?: Q
  query: ParsedUrlQuery
  preview?: boolean
  previewData?: any
}

getServerSideProps 函数

源码地址

还是先判断是否是动态路由

import { isDynamicRoute } from '../lib/router/utils/is-dynamic'
const pageIsDynamic = isDynamicRoute(pathname)

在 renderToHTML 函数中,await getServerSideProps

let data: UnwrapPromise<ReturnType<GetServerSideProps>>

try {
    data = await getServerSideProps({
        req,
        res,
        query,
        ...(pageIsDynamic ? { params: params as ParsedUrlQuery } : undefined),
        ...(previewData !== false
        ? { preview: true, previewData: previewData }
        : undefined),
    })
}

那上面的这个变量 params 哪来的呢?

在最开始的 renderToHTML 函数里面有定义
注意这里的参数 renderOpts,里面就有我们要的 params

renderToHTML 方法的 renderOpts 带有 params

export async function renderToHTML(
  req: IncomingMessage,
  res: ServerResponse,
  pathname: String,
  query: ParsedUrlQuery,
  renderOpts: RenderOpts
): Promise<string | null> {
  const {
    ....
    params,
  } = renderOpts
}

我们打印一下 renderOpts

renderOpts {
  App: [Function: App],
  Document: [Function: Document] { headTagsMiddleware: [Function] },
  Component: [Function: Index],
  buildManifest: {
    polyfillFiles: [ 'static/chunks/polyfills.js' ],
    devFiles: [ 'static/chunks/react-refresh.js' ],
    ampDevFiles: [ 'static/chunks/webpack.js', 'static/chunks/amp.js' ],
    lowPriorityFiles: [
      'static/development/_buildManifest.js',
      'static/development/_buildManifest.module.js',
      'static/development/_ssgManifest.js',
      'static/development/_ssgManifest.module.js'
    ],
    pages: {
      '/_app': [Array],
      '/_error': [Array],
      '/next/dist/pages/_error': [Array],
      '/ssr/[id]': [Array]
    },
    ampFirstPages: []
  },
  reactLoadableManifest: { './dev/noop': [ [Object] ] },
  pageConfig: {},
  getServerSideProps: [AsyncFunction: getServerSideProps],
  getStaticProps: undefined,
  getStaticPaths: undefined,
  poweredByHeader: true,
  canonicalBase: '',
  buildId: 'development',
  generateEtags: true,
  previewProps: {
    previewModeId: '60a3bcf190eab83416606d193122f497',
    previewModeSigningKey: '3b14badc567cf7f58956a0887bded6d36c8b9bfa4de4bafcd0dd968c1f50561e',
    previewModeEncryptionKey: '7de719bbe85326be17f0ac1c3c544069ff1e2000d7037f43e8284f32d9ed15a7'
  },
  customServer: undefined,
  ampOptimizerConfig: undefined,
  basePath: '',
  optimizeFonts: false,
  fontManifest: null,
  assetPrefix: '',
  dev: true,
  ErrorDebug: [Function: ReactDevOverlay],
  ampSkipValidation: false,
  ampValidator: [Function],
  params: { id: 'server-side-rendered2' },
  isDataReq: false
}

render 方法不带有 params?

这里的方法来自 next/next-server/server/next-server.ts
调用 this.renderToHTML 的时候没有第五个参数 renderOpts

注意:这里是 this.renderToHTML,而不是 renderToHTML

import {  renderToHTML } from './render'

public async render(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
    parsedUrl?: UrlWithParsedQuery
  ): Promise<void> {
    const html = await this.renderToHTML(req, res, pathname, query)
    // Request was ended by the user
    if (html === null) {
      return
    }

    return this.sendHTML(req, res, html)
  }

我们看一下 next-server.ts 里面确实有一个方法 renderToHTML

public async renderToHTML(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {}
  ): Promise<string | null> {
   try {
      const result = await this.findPageComponents(pathname, query)
   } catch (err) {
      // ...
   }

}

上面的 findPageComponents 函数是做什么的?

next-server.ts 有一个 privatefindPageComponents 函数

接受 3 个参数:

  • pathname
  • query
  • params
private async findPageComponents(
    pathname: string,
    query: ParsedUrlQuery = {},
    params: Params | null = null
  ): Promise<FindComponentsResult | null> {
}

使用方式一:

result = await this.findPageComponents('/404')

使用方式二:

result = await this.findPageComponents('/_error', query)

这里:

  • pathname ==> /ssr/server-side-rendered2
  • query ==> {}

findPageComponents 函数内部:
下面的 paths 返回的是[ '/ssr/server-side-rendered2' ]

这里因为动态路由,所以 this.findPageComponent 返回了 null

const paths = [
      // try serving a static AMP version first
      query.amp ? normalizePagePath(pathname) + '.amp' : null,
      pathname,
    ].filter(Boolean)

循环 paths,调用 loadComponents

第一次,因为是动态路由,所以是不存在的

pagePath ==> /ssr/server-side-rendered2

import { loadComponents } from './load-components'

for (const pagePath of paths) {
    try {
        const components = await loadComponents(
        this.distDir,
        pagePath!,
        !this.renderOpts.dev && this._isLikeServerless
        )
        return {
            components,
            query: {
                ...(components.getStaticProps
                ? { _nextDataReq: query._nextDataReq, amp: query.amp }
                : query),
                ...(params || {}),
            },
        }
    } catch (err) {
        if (err.code !== 'ENOENT') throw err
    }
}

看一下 loadComponents 是做什么的?

export async function loadComponents(
  distDir: string,
  pathname: string,
  serverless: boolean
): Promise<LoadComponentsReturnType> {
}

参数:

  • distDir -- '/***/.next'
  • pathname -- '/ssr/server-side-rendered2'
  • serverless -- false

如果 findPageComponents 返回 null 之后,接着走 renderToHTML

判断是否有 dynamicRoutes,循环 dynamicRoutes

这里的 dynamicRoutes 为 { page: '/ssr/[id]', match: [Function] }

if (this.dynamicRoutes) {
  for (const dynamicRoute of this.dynamicRoutes) {
      // ...
  }
}

从路由里面获取 params

第一步 - 配置项 useFileSystemPublicRoutes

const defaultConfig: { [key: string]: any } = {
  useFileSystemPublicRoutes: true,
}

第二步 - getDynamicRoutes

从 nextConfig 里面获取配置项 useFileSystemPublicRoutes

const {
     useFileSystemPublicRoutes
} = this.nextConfig;

if (useFileSystemPublicRoutes) {
    this.dynamicRoutes = this.getDynamicRoutes();
}

对应的实现

protected getDynamicRoutes() {
  return getSortedRoutes(Object.keys(this.pagesManifest!))
    .filter(isDynamicRoute)
    .map((page) => ({
    page,
    match: getRouteMatcher(getRouteRegex(page)),
    }))
}

在 for 循环内部

这里的 dynamicRoute 来 match pathname

const params = dynamicRoute.match(pathname)
  • dynamicRoute -- { page: '/ssr/[id]', match: [Function] }
  • pathname -- /ssr/server-side-rendered2

params { id: 'server-side-rendered2' }

useFileSystemPublicRoutes 的作用是什么?

用途是什么?

因为我们大部分的业务应用场景是 Custom Server,所以会关注到这个配置项,先看一下文档

默认情况下,next 会自动在服务端给 pages 目录下的文件生成对应的访问地址,但是:

disables filename routes from SSR; client-side routing may still access those paths.

module.exports = {
  useFileSystemPublicRoutes: false
}

客户端如何做路由拦截

使用 next/routerbeforePopState

router.beforePopState(cb)

参数 cb 返回 false,就不会执行 popstate

具体的参数如下:

router.beforePopState(({ url, as, options }) => {
})

Nuxt layout相关

nuxt layout相关

使用

官方文档 layouts目录介绍

一定要有<Nuxt />组件, 用于"actually include the page component"

  • default.vue

    默认layout, 如果page没有指定layout则使用default.vue

  • custom layout

    • 所有/layouts目录下的文件(top-level)都是custom layout

    • 为page指定layout

      可用string / function

      使用function来指定layout时, 可根据上下文context指定不同layout

  • error.vue
    虽在layouts目录下,但不是一个layout,而是一个page,所以无<Nuxt />组件,且仍可以为这个error.vue指定layout

    • 如何切换到error page

        context.error({
          statusCode: 404,
          message: 'page not found'
        })
    • 在error page中使用传入的error对象

      context.error()中传入的error对象可以在error.vue中通过声明 props: ['error'] 来接收,进而可以通过error.statusCode或error.message做不同展示

      <template>
        <div class="container">
          <h1 v-if="error.statusCode === 404">Page not found</h1>
          <h1 v-else>An error occurred</h1>
        </div>
      </template>
      
      <script>
        export default {
          props: ['error'],
        }
      </script>

error layout, context.error相关源码

.nuxt/index.js
取/layouts下的error.vue

import NuxtError from '../layouts/error.vue'
export { NuxtError }

setContext, 给context.error赋值

// Set context to app.context
await setContext(app, {
  //...
  error: app.nuxt.error.bind(app),
  ssrContext,
  //...
})

context.error将会被赋值为下面的nuxt.error

context.error()传入的对象会被赋值给nuxt.err和ssrContext.nuxt.error, 并把app.context._errored赋值为true

const app = {
  //...
  nuxt: {
    err: null,
    dateErr: null,
    error (err) {
      err = err || null
      app.context._errored = Boolean(err)
      err = err ? normalizeError(err) : null
      let nuxt = app.nuxt // to work with @vue/composition-api, see https://github.com/nuxt/nuxt.js/issues/6517#issuecomment-573280207
      if (this) {
        nuxt = this.nuxt || this.$options.nuxt
      }
      nuxt.dateErr = Date.now()
      nuxt.err = err
      // Used in src/server.js
      if (ssrContext) {
        ssrContext.nuxt.error = err
      }
      return err
    }
  },
}

上面的setContext是从utils中引入
./nuxt/utils.js

export async function setContext (app, context) {
  // If context not defined, create it
  if (!app.context) {
    app.context = {
      //...
      error: context.error,
      //...
    }
    if (context.ssrContext) {
      app.context.ssrContext = context.ssrContext
    }
  }
  //...
}

.nuxt/app.js

render函数中,nuxt.err存在则会setLayout为error page声明的layout(若是函数类型则会传入context计算)

  render (h, props) {
    //...
    if (this.nuxt.err && NuxtError) {
      const errorLayout = (NuxtError.options || NuxtError).layout
      if (errorLayout) {
        this.setLayout(
          typeof errorLayout === 'function'
            ? errorLayout.call(NuxtError, this.context)
            : errorLayout
        )
      }
    }
    //...
  },

setLayout函数可以看出如果没有声明layout或声明的layout不存在则会使用'default'

setLayout (layout) {
  if(layout && typeof layout !== 'string') {
    throw new Error('[nuxt] Avoid using non-string value as layout property.')
  }

  if (!layout || !layouts['_' + layout]) {
    layout = 'default'
  }
  this.layoutName = layout
  this.layout = layouts['_' + layout]
  return this.layout
},

精读 babel-loader

使用

先看一下我们的配置:

babel.config.json

项目根目录包含配置 json

{
  "presets": ["@babel/env", "@babel/typescript"],
  "plugins": [

    ["@babel/plugin-transform-typescript", {
      "allowNamespaces": true
    }],
    "@babel/plugin-transform-regenerator",
    "@babel/plugin-transform-async-to-generator",
    "@babel/plugin-proposal-class-properties"
  ]
}

关于配置,可以查看中文版地址

里面提到一个关键点:

你是否需要编译 node_modules? -- 那么 babel.config.json 文件可以满足你的的需求!

这边抛一个问题:

.babelrc 配置内容支持编译 node_modules 吗?

webpack 配置 babel-loader

module: {
  rules: [
    {
      test: '/\.ts|\.js|\.mjs$/',
      use: [
          {
            loader: 'babel-loader',
            options: {
              cacheDirectory: true
            },
          }
       ]
    }
  ]
}

babel-loader 源码版本

主要是针对 8.1.0源码地址

babel-loader源码目录结构

目录

核心依赖 @babel/core

后面会提到具体使用了什么方法

let babel;
try {
  babel = require("@babel/core");
} catch (err) {
  if (err.code === "MODULE_NOT_FOUND") {
    err.message +=
      "\n babel-loader@8 requires Babel 7.x (the package '@babel/core'). " +
      "If you'd like to use Babel 6.x ('babel-core'), you should install 'babel-loader@7'.";
  }
  throw err;
}

第一步:获取插件配置

源码地址:如下
通过 loader-utilsgetOptions

const loaderUtils = require("loader-utils")

async function loader(source, inputSourceMap, overrides) {
  let loaderOptions = loaderUtils.getOptions(this) || {};
  // ...
}

第二步:验证插件配置

用到了第三方工具包schema-utils,配合 schema.json

const validateOptions = require("schema-utils")
const schema = require("./schema")

async function loader(source, inputSourceMap, overrides) {
  // ...
  validateOptions(schema, loaderOptions, {
    name: "Babel loader",
  })
}

schema.json 文件

源码如下
官方对于 options 里面的配置项也有说明:地址

整体是一个对象,包含属性:

  • cacheDirectory

Default false. When set, the given directory will be used to cache the results of the loader. Future webpack builds will attempt to read from the cache to avoid needing to run the potentially expensive Babel recompilation process on each run. If the value is set to true in options ({cacheDirectory: true}), the loader will use the default cache directory in node_modules/.cache/babel-loader or fallback to the default OS temporary file directory if no node_modules folder could be found in any root directory.

  • cacheIdentifier

Default is a string composed by the @babel/core's version, the babel-loader's version, the contents of .babelrc file if it exists, and the value of the environment variable BABEL_ENV with a fallback to the NODE_ENV environment variable. This can be set to a custom value to force cache busting if the identifier changes.

  • cacheCompression

Default true. When set, each Babel transform output will be compressed with Gzip. If you want to opt-out of cache compression, set it to false -- your project may benefit from this if it transpiles thousands of files.

  • customize

Default null. The path of a module that exports a custom callback like the one that you'd pass to .custom(). Since you already have to make a new file to use this, it is recommended that you instead use .custom to create a wrapper loader. Only use this if you must continue using babel-loader directly, but still want to customize.

{
  "type": "object",
  "properties": {
    "cacheDirectory": {
      "oneOf": [
        {
          "type": "boolean"
        },
        {
          "type": "string"
        }
      ],
      "default": false
    },
    "cacheIdentifier": {
      "type": "string"
    },
    "cacheCompression": {
      "type": "boolean",
      "default": true
    },
    "customize": {
      "type": "string",
      "default": null
    }
  },
  "additionalProperties": true
}

如何获取全局的配置

调用了 babel.loadPartialConfig

const config = babel.loadPartialConfig(
    injectCaller(programmaticOptions, this.target),
  );

我们尝试打印一下上面的 config:

{
  options: {
    filename: '/**/ts/bezel.ts',
    sourceMaps: false,
    sourceFileName: '/**/src/ts/bezel.ts',
    caller: {
      name: 'babel-loader',
      target: 'web',
      supportsStaticESM: true,
      supportsDynamicImport: true,
      supportsTopLevelAwait: true
    },
    cloneInputAst: true,
    babelrc: false,
    configFile: false,
    passPerPreset: false,
    envName: 'production',
    cwd: '/**',
    root: '/**',
    plugins: [ [ConfigItem], [ConfigItem], [ConfigItem], [ConfigItem] ],
    presets: [ [ConfigItem], [ConfigItem] ]
  },
  babelignore: undefined,
  babelrc: undefined,
  config: '/**/babel.config.json'
}

具体看一下 loadPartialConfig

地址如下
导出的 loadPartialConfig 来自 partial.js

import { loadPartialConfig as loadPartialConfigRunner } from "./partial";

const maybeErrback = runner => (opts: mixed, callback: Function) => {
  if (callback === undefined && typeof opts === "function") {
    callback = opts;
    opts = undefined;
  }
  return callback ? runner.errback(opts, callback) : runner.sync(opts);
};

export const loadPartialConfig = maybeErrback(loadPartialConfigRunner);

查看 loadPartialConfig 的实现细节:

依赖了 loadPrivatePartialConfig 函数

export const loadPartialConfig = gensync<[any], PartialConfig | null>(
  function* (inputOpts: mixed): Handler<PartialConfig | null> {
    const result: ?PrivPartialConfig = yield* loadPrivatePartialConfig(
      inputOpts,
    );
    if (!result) return null;

    const { options, babelrc, ignore, config } = result;

    (options.plugins || []).forEach(item => {
      if (item.value instanceof Plugin) {
        throw new Error(
          "Passing cached plugin instances is not supported in " +
            "babel.loadPartialConfig()",
        );
      }
    });

    return new PartialConfig(
      options,
      babelrc ? babelrc.filepath : undefined,
      ignore ? ignore.filepath : undefined,
      config ? config.filepath : undefined,
    );
  },
);

loadPrivatePartialConfig

export default function* loadPrivatePartialConfig(
  inputOpts: mixed,
): Handler<PrivPartialConfig | null> {
}
envName

返回的 options 里面有一个 envName 字段

import { getEnv } from "./helpers/environment";

envName = getEnv()

getEnv 方法的内部实现:

export function getEnv(defaultValue: string = "development"): string {
  return process.env.BABEL_ENV || process.env.NODE_ENV || defaultValue;
}
configChain

依赖了 config-chain.js 提供的 buildRootChain 方法

import { buildRootChain, type ConfigContext } from "./config-chain";

const context: ConfigContext = {
    filename,
    cwd: absoluteCwd,
    root: absoluteRootDir,
    envName,
    caller,
    showConfig: showConfigPath === filename,
  };

  const configChain = yield* buildRootChain(args, context);

我们可以打印一下 configChain 返回的值:

{
  plugins: [
    {
      name: undefined,
      alias: '/**/node_modules/@babel/plugin-transform-typescript/lib/index.js',
      value: [Function],
      options: [Object],
      dirname: '/**',
      ownPass: false,
      file: [Object]
    },
    {
      name: undefined,
      alias: '/**/node_modules/@babel/plugin-transform-regenerator/lib/index.js',
      value: [Function: _default],
      options: undefined,
      dirname: '/**',
      ownPass: false,
      file: [Object]
    },
    {
      name: undefined,
      alias: '/**/node_modules/@babel/plugin-transform-async-to-generator/lib/index.js',
      value: [Function],
      options: undefined,
      dirname: '/**',
      ownPass: false,
      file: [Object]
    },
    {
      name: undefined,
      alias: '/**/node_modules/@babel/plugin-proposal-class-properties/lib/index.js',
      value: [Function],
      options: undefined,
      dirname: '/**',
      ownPass: false,
      file: [Object]
    }
  ],
  presets: [
    {
      name: undefined,
      alias: '/**/node_modules/@babel/preset-env/lib/index.js',
      value: [Function],
      options: undefined,
      dirname: '/**',
      ownPass: false,
      file: [Object]
    },
    {
      name: undefined,
      alias: '/**/node_modules/@babel/preset-typescript/lib/index.js',
      value: [Function],
      options: undefined,
      dirname: '/**',
      ownPass: false,
      file: [Object]
    }
  ],
  options: [
    {},
    {
      filename: '/**/src/ts/player.ts',
      inputSourceMap: undefined,
      sourceMaps: false,
      sourceFileName: '/**/src/ts/player.ts',
      caller: [Object]
    }
  ],
  ignore: undefined,
  babelrc: undefined,
  config: {
    filepath: '/**/babel.config.json',
    dirname: '/**',
    options: { presets: [Array], plugins: [Array] }
  }
}

重点关注 config 的值如何获取

let configFile;
if (typeof opts.configFile === "string") {
    configFile = yield* loadConfig(
      opts.configFile,
      context.cwd,
      context.envName,
      context.caller,
    );
} else if (opts.configFile !== false) {
    configFile = yield* findRootConfig(
      context.root,
      context.envName,
      context.caller,
    );
}

loadConfig

export function* loadConfig(
  name: string,
  dirname: string,
  envName: string,
  caller: CallerMetadata | void,
): Handler<ConfigFile> {
  const filepath = yield* resolve(name, { basedir: dirname });

  const conf = yield* readConfig(filepath, envName, caller);
  if (!conf) {
    throw new Error(`Config file ${filepath} contains no configuration data`);
  }

  debug("Loaded config %o from %o.", name, dirname);
  return conf;
}

findRootConfig

依赖了 loadOneConfig

function* loadOneConfig(
  names: string[],
  dirname: string,
  envName: string,
  caller: CallerMetadata | void,
  previousConfig?: ConfigFile | null = null,
): Handler<ConfigFile | null> {
  const configs = yield* gensync.all(
    names.map(filename =>
      readConfig(path.join(dirname, filename), envName, caller),
    ),
  );
  const config = configs.reduce((previousConfig: ConfigFile | null, config) => {
    if (config && previousConfig) {
      throw new Error(
        `Multiple configuration files found. Please remove one:\n` +
          ` - ${path.basename(previousConfig.filepath)}\n` +
          ` - ${config.filepath}\n` +
          `from ${dirname}`,
      );
    }

    return config || previousConfig;
  }, previousConfig);

  if (config) {
    debug("Found configuration %o from %o.", config.filepath, dirname);
  }
  return config;
}

export function findRootConfig(
  dirname: string,
  envName: string,
  caller: CallerMetadata | void,
): Handler<ConfigFile | null> {
  return loadOneConfig(ROOT_CONFIG_FILENAMES, dirname, envName, caller);
}

ROOT_CONFIG_FILENAMES 常量数组

export const ROOT_CONFIG_FILENAMES = [
  "babel.config.js",
  "babel.config.cjs",
  "babel.config.mjs",
  "babel.config.json",
];

RELATIVE_CONFIG_FILENAMES 常量数组

也在babel-core/src/config/files/configuration.js

const RELATIVE_CONFIG_FILENAMES = [
  ".babelrc",
  ".babelrc.js",
  ".babelrc.cjs",
  ".babelrc.mjs",
  ".babelrc.json",
];

扩展链接

nuxtjs 在非线上环境设置 HMR

相比本地开发,我们会开启 HMR:

配置

dev 配置

Define the development or production mode of Nuxt.js.

浏览器

打开控制台的 console,有一段[HMR] connected

如图:
如图

在 node_modules/@nuxt/webpack/dist/webpack.js

if (this.dev) {
     // TODO: webpackHotUpdate is not defined: https://github.com/webpack/webpack/issues/6693
     plugins.push(new webpack__default.HotModuleReplacementPlugin());
}

扩展:Webpack 4. Uncaught ReferenceError: webpackHotUpdate is not defined #6693

HotModuleReplacementPlugin

文档地址:如下

HMR

文档参考:地址

hot module replacement 模块热替换 - 会在应用程序运行过程中,替换、添加或删除 模块,而无需重新加载整个页面。主要是通过以下几种方式,来显著加快开发速度:

  • 保留在完全重新加载页面期间丢失的应用程序状态。
  • 只更新变更内容,以节省宝贵的开发时间。
  • 在源代码中 CSS/JS 产生修改时,会立刻在浏览器中进行更新,这几乎相当于在浏览器 devtools 直接更改样式。

module.hot

文档参考:地址

模块热替换是否启用,并给进程提供一个接口

module.hot.accept

接受(accept)给定 依赖模块(dependencies) 的更新,并触发一个 回调函数 来响应更新。

语法:

module.hot.accept(
  dependencies, // 可以是一个字符串或字符串数组
  callback // 用于在模块更新后触发的函数
)

vuex 热重载

.nuxt/store.js

if (process.client && module.hot) {
  module.hot.accept([
    '../store/index.js',
    '../store/modules/global.js'
  ], () => {
    window.$nuxt.$store.hotUpdate(store)
  })
}

官方文档:地址 使用的store.hotUpdate

EventSource

MDN 文档:地址

服务器推送的一个网络事件接口,一个EventSource实例会对HTTP服务开启一个持久化的连接,以text/event-stream 格式发送事件, 会一直保持开启直到被要求关闭。

React 事件

17 中的事件

不再向 document 附加事件处理器,会将事件处理器附加到渲染 React 树的根 DOM 容器中

image

onScroll

不再冒泡,

onFocus 和 onBlur

在底层切换为原生的 focusinfocusout,总是冒泡的
demo 地址

event-pooling 事件池

官方文档:https://zh-hans.reactjs.org/docs/legacy-event-pooling.html

捕获事件 onClickCapture

demo 地址如下

SyntheticEvent

image

官方文档地址

浏览器原生事件的跨浏览器包装器,从 17 开始 e.persist() 不再生效,不再放入事件池

核心方法 dispatchEvent

参考地址;

竞品的优秀案例之动态 placeholder

haokan 视频动态搜索框热词

地址:点击

效果:
image

image

核心代码:

image

function processText (e) {
  var t, n = this;
   this.isPlaying = !0,
       n.typeString(n.texts[e], (function() {
                n.timeouts.length = 0,
                n.options.loop || n.texts[e + 1] || (n.isPlaying = !1),
                t = setTimeout((function() {
                    n.processText(n.options.loop ? (e + 1) % n.texts.length : e + 1)
                }
                ), n.options.sentenceDelay),
                n.timeouts.push(t)
            }
      ))
}
function typeString (e, t) {
  var n, r = this;
            if (!e)
                return !1;
            function i(n) {
                r.el.setAttribute("placeholder", e.substr(0, n + 1) + (n !== e.length - 1 && r.options.showCursor ? r.options.cursor : "")),
                n === e.length - 1 && t()
            }
            for (var o = 0; o < e.length; o++)
                n = setTimeout(i, o * r.options.letterDelay, o),
                r.timeouts.push(n)
}

完整代码:

function(e, t, n) {
    !function() {
        var t = "placeholder"in document.createElement("input");
        var n = Object.freeze({
            START: "start",
            STOP: "stop",
            NOTHING: !1
        })
          , r = {
            letterDelay: 100,
            sentenceDelay: 1e3,
            loop: !1,
            startOnFocus: !0,
            shuffle: !1,
            showCursor: !0,
            cursor: "|",
            autoStart: !1,
            onFocusAction: n.START,
            onBlurAction: n.STOP
        };
        function i(e, t, i) {
            var o, a;
            if (this.el = e,
            this.texts = t,
            i = i || {},
            this.options = function(e, t) {
                var n = {};
                for (var r in e)
                    n[r] = void 0 === t[r] ? e[r] : t[r];
                return n
            }(r, i),
            this.options.startOnFocus || (console.warn("Superplaceholder.js: `startOnFocus` option has been deprecated. Please use `onFocusAction`, `onBlurAction` and `autoStart`"),
            this.options.autoStart = !0,
            this.options.onFocusAction = n.NOTHING,
            this.options.onBlurAction = n.NOTHING),
            this.timeouts = [],
            this.isPlaying = !1,
            this.options.shuffle)
                for (var s = this.texts.length; s--; )
                    a = ~~(Math.random() * s),
                    o = this.texts[a],
                    this.texts[a] = this.texts[s],
                    this.texts[s] = o;
            this.begin()
        }
        i.prototype.begin = function() {
            this.originalPlaceholder = this.el.getAttribute("placeholder"),
            (this.options.onFocusAction || this.options.onBlurAction) && (this.listeners = {
                focus: this.onFocus.bind(this),
                blur: this.onBlur.bind(this)
            },
            this.el.addEventListener("focus", this.listeners.focus),
            this.el.addEventListener("blur", this.listeners.blur)),
            this.options.autoStart && this.processText(0)
        }
        ,
        i.prototype.onFocus = function() {
            if (this.options.onFocusAction === n.START) {
                if (this.isInProgress())
                    return;
                this.processText(0)
            } else
                this.options.onFocusAction === n.STOP && this.cleanUp()
        }
        ,
        i.prototype.onBlur = function() {
            if (this.options.onBlurAction === n.STOP)
                this.cleanUp();
            else if (this.options.onBlurAction === n.START) {
                if (this.isInProgress())
                    return;
                this.processText(0)
            }
        }
        ,
        i.prototype.cleanUp = function() {
            for (var e = this.timeouts.length; e--; )
                clearTimeout(this.timeouts[e]);
            null === this.originalPlaceholder ? this.el.removeAttribute("placeholder") : this.el.setAttribute("placeholder", this.originalPlaceholder),
            this.timeouts.length = 0,
            this.isPlaying = !1
        }
        ,
        i.prototype.isInProgress = function() {
            return this.isPlaying
        }
        ,
        i.prototype.typeString = function(e, t) {
            var n, r = this;
            if (!e)
                return !1;
            function i(n) {
                r.el.setAttribute("placeholder", e.substr(0, n + 1) + (n !== e.length - 1 && r.options.showCursor ? r.options.cursor : "")),
                n === e.length - 1 && t()
            }
            for (var o = 0; o < e.length; o++)
                n = setTimeout(i, o * r.options.letterDelay, o),
                r.timeouts.push(n)
        }
        ,
        i.prototype.processText = function(e) {
            var t, n = this;
            this.isPlaying = !0,
            n.typeString(n.texts[e], (function() {
                n.timeouts.length = 0,
                n.options.loop || n.texts[e + 1] || (n.isPlaying = !1),
                t = setTimeout((function() {
                    n.processText(n.options.loop ? (e + 1) % n.texts.length : e + 1)
                }
                ), n.options.sentenceDelay),
                n.timeouts.push(t)
            }
            ))
        }
        ;
        var o = function(e) {
            if (t) {
                var n = new i(e.el,e.sentences,e.options);
                return {
                    start: function() {
                        n.isInProgress() || n.processText(0)
                    },
                    stop: function() {
                        n.cleanUp()
                    },
                    destroy: function() {
                        for (var e in n.cleanUp(),
                        n.listeners)
                            n.el.removeEventListener(e, n.listeners[e])
                    }
                }
            }
        };
        o.Actions = n,
        e.exports = o
    }()

这里面提到了一个不错的工具包:superplaceholder.js

nuxtjs 相关

x-content-type-options

导致静态资源访问都出现 500

参考地址:MDN 文档

正常的,如图:

response css

错误的,如图:

response css

Strapi

Strapi 是什么?

the open-source Headless CMS (opens new window)developers love.

创建 Content-Type

这个是创建表时默认结尾会加个s,在高级设置中可以自己设置表名

image

访问方式

Method Path Description
GET /{content-type} Get a list of {content-type} entries
GET /{content-type}/:id Get a specific {content-type} entry
GET /{content-type}/count Count {content-type} entries
POST /{content-type} Create a {content-type} entry
DELETE /{content-type}/:id Delete a {content-type} entry
PUT /{content-type}/:id Update a {content-type} entry

Single Types

api 文件夹

config 文件夹

config/functions/bootstrap.js

官网地址

The bootstrap function is called at every server start. You can use it to add a specific logic at this moment of your server's lifecycle.

我们在这里打印一下 strapi:

{
  reload: {}
  app: {},
  router: {},
  server: {},
  log: {},
  utils: {},
  dir: '',
  admin: {},
  plugins: {},
  config: {},
  isLoaded: false,
  fs: {}
  eventHub: {},
  api: {},
  components: {},
  middleware: {},
  hook: {},
  connections: {},
  contentTypes: {},
  models: {},
  controllers: {},
  services: {},
  webhookRunner: {},
  db: {},
  store: [Function],
  webhookStore: {},
  entityValidator: {},
  entityService: {},
  telemetry: { send: [AsyncFunction: send] },
  errors: [Function: Error] 
}

打印一下:strapi.app.use

use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    if (isGeneratorFunction(fn)) {
      deprecate('Support for generators will be removed in v3. ' +
                'See the documentation for examples of how to convert old middleware ' +
                'https://github.com/koajs/koa/blob/master/docs/migration.md');
      fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn);
    return this;
  }

自定义404

config/functions/responses/404.js

'use strict';

module.exports = async ( ctx ) => {
  return ctx.notFound('My custom message 404');
};

访问一个不存在的地址:

{"statusCode":404,"error":"Not Found","message":"My custom message 404"}

命令

strapi develop

strapi build

strapi start

plugin

strapi-plugin-content-manager

babel 相关

stage 0 之类

@babel/preset-stage-0
As of v7.0.0-beta.55, we've removed Babel's Stage presets.

Please consider reading our blog post on this decision for more details. TL;DR is that it's more beneficial in the long run to explicitly add which proposals to use.

地址说明: @babel/preset-stage-0

nuxt 处理 layout

resolveLayouts

packages/builder/src/builder.js

async resolveLayouts ({ templateVars, templateFiles }) {
  if (await fsExtra.exists(path.resolve(this.options.srcDir, this.options.dir.layouts))) {
     for (const file of await this.resolveFiles(this.options.dir.layouts)) {

     }
  }
}

nuxtjs 的 nuxt-link 的 no-prefetch 的原理剖析

nuxt-link 的 no-prefetch

官方文档的解释:地址

However sometimes you may want to disable prefetching on some links if your page has a lot of JavaScript or you have a lot of different pages that would be prefetched or you have a lot of third party scripts that need to be loaded. To disable the prefetching on a specific link, you can use the no-prefetch prop.

注意一个版本变化:

Since Nuxt.js v2.10.0, you can also use the prefetch prop set to false

单个设置 false

<NuxtLink to="/about" no-prefetch>About page not pre-fetched</NuxtLink>
<NuxtLink to="/about" :prefetch="false">About page not pre-fetched</NuxtLink>

全局设置

export default {
  router: {
    prefetchLinks: false
  }
}

nuxt-link 源码

地址

  • 接受 props:prefetch 和 noPrefetch
export default {
  name: 'NuxtLink',
  props: {
    prefetch: {},
    noPrefetch: {},
  },
  mounted () {
    if (this.prefetch && !this.noPrefetch) {
      this.handleId = requestIdleCallback(this.observe, { timeout: 2e3 })
    }
  }  
}

requestIdleCallback

MDN 文档:地址

将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序。

const requestIdleCallback = window.requestIdleCallback ||
  function (cb) {
    const start = Date.now()
    return setTimeout(function () {
      cb({
        didTimeout: false,
        timeRemaining: () => Math.max(0, 50 - (Date.now() - start))
      })
    }, 1)
  }

observe

methods 里面的

methods: {
  observe () {
    if (!observer) {
      return
    }
    if (this.shouldPrefetch()) {
        this.$el.__prefetch = this.prefetchLink.bind(this)
        observer.observe(this.$el)
        this.__observed = true
      }
  }
}

变量 observer

MDN 地址

提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。祖先元素与视窗(viewport)被称为根(root)。当一个IntersectionObserver对象被创建时,其被配置为监听根中一段给定比例的可见区域。一旦IntersectionObserver被创建,则无法更改其配置,所以一个给定的观察者对象只能用来监听可见区域的特定变化值;然而,你可以在同一个观察者对象中配置监听多个目标元素。

const observer = window.IntersectionObserver && new window.IntersectionObserver((entries) => {
  entries.forEach(({ intersectionRatio, target: link }) => {
    if (intersectionRatio <= 0) {
      return
    }
    link.__prefetch()
  })
})

shouldPrefetch

shouldPrefetch () {
   return this.getPrefetchComponents().length > 0
}

getPrefetchComponents

getPrefetchComponents () {
  const ref = this.$router.resolve(this.to, this.$route, this.append)
  const Components = ref.resolved.matched.map(r => r.components.default)

  return Components.filter(
    Component => typeof Component === 'function' && 
  !Component.options && !Component.__prefetched)
}

核心方法 prefetchLink

先调用 canPrefetch

prefetchLink () {
  if (!this.canPrefetch()) {
    return
  }
  // ...
}

canPrefetch

这里用到了 navigator.connection

返回网络连接状态NetworkInformation对象,包括.downlink(网络下行速度) effectiveType(网络类型) onchange(有值代表网络状态变更) rtt(估算的往返时间) saveData(打开/请求数据保护模式)

canPrefetch () {
  const conn = navigator.connection
  const hasBadConnection = this.<%= globals.nuxt %>.isOffline || (conn && ((conn.effectiveType || '').includes('2g') || conn.saveData))

  return !hasBadConnection
}

beforeDestrory

这里还调用了 cancelIdleCallback

beforeDestroy () {
    cancelIdleCallback(this.handleId)

    if (this.__observed) {
      observer.unobserve(this.$el)
      delete this.$el.__prefetch
    }
  }

cancelIdleCallback

const cancelIdleCallback = window.cancelIdleCallback || function (id) {
  clearTimeout(id)
}

然后在调用 getPrefetchComponents

observer.unobserve(this.$el)
const Components = this.getPrefetchComponents()

循环 Components,设置一个 __prefetched

for (const Component of Components) {
  const componentOrPromise = Component()
  if (componentOrPromise instanceof Promise) {
    componentOrPromise.catch(() => {})
  }
  Component.__prefetched = true
}

升级 next 遇到的各种坑

1、报错

TypeError: Cannot read property 'concat' of undefined
    at Object.webpack (/Users/**/node_modules/next-transpile-modules/src/next-transpile-modules.js:102:73)
    at Object.webpack (/Users/**/@zeit/next-bundle-analyzer/index.js:34:27)
    at Object.webpack (/Users/**/node_modules/next-images/index.js:36:27)
    at getBaseWebpackConfig (/Users/**/node_modules/next/build/webpack-config.ts:1120:28)
    at async Promise.all (index 0)
    at HotReloader.start (/Users/**/node_modules/next/server/hot-reloader.ts:304:21)
    at DevServer.prepare (/Users/**/node_modules/next/server/next-dev-server.ts:263:5)

错误很明显:查看 next-transpile-modules

Next.js version Plugin version
Next.js 9.5+ 4.x
Next.js 9.2 3.x
Next.js 8 / 9 2.x
Next.js 6 / 7 1.x

PerformanceTiming

navigationStart

地址

一个返回代表一个时刻的 unsigned long long 型只读属性,为紧接着在相同的浏览环境下卸载前一个文档结束之时的 Unix毫秒时间戳

domainLookupEnd

地址

一个返回代表一个时刻的 unsigned long long 型只读属性,为解析域名结束时的 Unix毫秒时间戳。

nextjs 中的 getServerSideProps

在 Custom Server 模式下,能替代 getInitialProps 吗?

如何理解 Custom Server

官方给的 DEMO地址

  • 普通的项目基本都是 next start 启动的服务(start 内置的命令)
  • 自定义的服务(一般我们采用一个 nodejs 的 http 框架,比如我们的 koa
const Koa = require('koa')
const next = require('next')
const Router = require('@koa/router')

const port = parseInt(process.env.PORT, 10) || 3000
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

app.prepare().then(() => {
  const server = new Koa()
  const router = new Router()

  router.get('/a', async (ctx) => {
    await app.render(ctx.req, ctx.res, '/a', ctx.query)
    ctx.respond = false
  })

  router.get('/b', async (ctx) => {
    await app.render(ctx.req, ctx.res, '/b', ctx.query)
    ctx.respond = false
  })

  server.use(async (ctx, next) => {
    ctx.res.statusCode = 200
    await next()
  })

  server.use(router.routes())
  server.listen(port, () => {
    console.log(`> Ready on http://localhost:${port}`)
  })
})

使用方式

官方文档:地址

import { GetServerSideProps } from 'next'
export const getServerSideProps: GetServerSideProps = async context => {
  const {
    req
  }  = context
  // 获取动态路由里面的数据
  const params = req.params
}

为什么这里获取动态路由的值不是官方的说法:

context.params

看一下我们的场景:

服务端路由

router.get('/u/:id/followers', ctx => {
  return ctx.render('/u/[id]/followers')
}

这里有 2 点:

  • 这里的 router 呢,使用了我们封装的 koa2-router
  • 这里的 render 呢,使用了我们封装的next-koa

第一点:koa2-router 动态路由 path

解决为什么可以从 req.params 里面有动态参数?

import Router from 'koa2-router'
import { Context } from 'koa'

const router = new Router<any, Context>('www')

看一下 koa2-router 如何处理 url 里面有 :id

注册 get 方法,地址

methods.concat('all').forEach(function(method) {
  Router.prototype[method] = function(path) {
    var offset = 1
    if (typeof path === 'function') {
      path = '/'
      offset = 0
    }
    var route = this.route(path)
    route[method].apply(route, slice.call(arguments, offset))
    return this
  }
})

这里会调用 Router.prototype.route地址

这里依赖了 Layer

var Layer = require('./layer')

Router.prototype.route = function route(path) {
  var route = new Route(path)

  var layer = new Layer(path, {
    sensitive: this.caseSensitive,
    strict: this.strict,
    end: true
  }, route.handle.bind(route))

  layer.route = route

  this.stack.push(layer)
  return route
}

我们打印一下 Layer

layer Layer {
  handler: [Function: bound handle] AsyncFunction,
  name: 'bound handle',
  keys: [
    {
      name: 'id',
      prefix: '/',
      suffix: '',
      pattern: '[^\\/#\\?]+?',
      modifier: ''
    }
  ],
  regexp: /^\/u(?:\/([^\/#\?]+?))\/following[\/#\?]?$/i {
    fast_star: false,
    fast_slash: false
  },
  path: '/u/10153573/following',
  params: { id: '10153573' },
  route: Route {
    path: '/u/:id/following',
    stack: [ [Layer] ],
    methods: { get: true }
  }
}

具体实现过程:地址

Layer.prototype.match = function match(path) {
   var match
   if (path != null) {
      match = this.regexp.exec(path)
   }

}

再往上:这里用到了工具包path-to-regexp

var pathToRegexp = require('path-to-regexp').pathToRegexp

function Layer(path, options, fn) {
  this.regexp = pathToRegexp(path, this.keys = [], opts)
}

看一下 match 的值:

[
  '/u/10153573/following',
  '10153573',
  index: 0,
  input: '/u/10153573/following',
  groups: undefined
]

循环 match

var keys = this.keys

for (var i = 1; i < match.length; i++) {
    var key = keys[i - 1]
    var prop = key.name
    var val = decode_param(match[i])

    if (val !== undefined || !(hasOwnProperty.call(params, prop))) {
      params[prop] = val
    }
  }

第二点: render

function render(this: Context, view: string, data?: any, parsed?: UrlWithParsedQuery) {
    return fixCtxUrl(this, data, parsed, (ctx, query, parsedUrl, state) => {
      if (!ctx.response._explicitStatus) {
        ctx.status = 200
      }
      if (isNextFetch(ctx)) {
        ctx.body = state
      } else {
        return app.render(ctx.req, ctx.res, view, query, parsedUrl)
      }
    })
  }

这里的 app.render

import next from 'next'

export default function NextKoa(options: NextKoaOptions = {}): NextApp {
  const app = next(opt) as 
}

官方文档中对于 app.render 的说明

从源码角度认识它

load-components.ts 文件中:loadComponents 方法,会处理服务端的方法,比如 getServerSideProps

export async function loadComponents(
  distDir: string,
  pathname: string,
  serverless: boolean 
): Promise<LoadComponentsReturnType> {
  // serverless 为 true
  if (serverless) {
    const Component = await requirePage(pathname, distDir, serverless)
    const { getStaticProps, getStaticPaths, getServerSideProps } = Component
    return {
      Component,
      pageConfig: Component.config || {},
      getStaticProps,
      getStaticPaths,
      getServerSideProps
    } as LoadComponentsReturnType
  }
}

上面的 LoadComponentsReturnType

export type LoadComponentsReturnType = {
  Components: React.ComponentType,
  // ...
  getServerSideProps?: GetServerSideProps
}

babel 编译之后是什么?

({
  "./pages/ssr/[id]/index.tsx":  (function(module, __webpack_exports__, __webpack_require__) {
   "use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ 
__webpack_require__.d(__webpack_exports__, \"default\", function() { return Index; });\n/* harmony export (binding) */ 
__webpack_require__.d(__webpack_exports__, \"getServerSideProps\", function() { return getServerSideProps; });\n
/* harmony import */ 
var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ \"react\");\n
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);\n
var _jsxFileName = \"/****/pages/ssr/[id]/index.tsx\";\n\n
var __jsx = react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement;\nfunction Index() {\n  
return __jsx(react__WEBPACK_IMPORTED_MODULE_0___default.a.Fragment, null, __jsx(\"p\", {\n   
 __self: this,\n    __source: {\n      fileName: _jsxFileName,\n      lineNumber: 3,\n      columnNumber: 7\n    }\n  }, \"[id] \\u76EE\\u5F55\\u8DEF\\u7531\\u4F5C\\u4E3A\\u52A8\\u6001\\u53C2\\u6570\"));\n}\n
async function getServerSideProps(context) {\n  // console.log('context', context)\n  return {\n    props: {}\n  };\n}");

/***/ })
})

常见错误一

getServerSideProps can not be attached to a page's component and must be exported from the page. 
See more info here: https://err.sh/next.js/gssp-component-member

babel 中的 babelify 和 babel-register

browserify 是什么

browser-side require() the node.js way. Browserify lets you require('modules') in the browser by bundling up all of your dependencies.

官方的 handbook 地址

处理 nodejs 核心包

In order to make more npm modules originally written for node work in the browser, browserify provides many browser-specific implementations of node core libraries:

  • assert
  • buffer
  • console
  • constants
  • crypto
  • domain
  • events
  • http
  • https
  • os
  • path
  • punycode
  • querystring
  • stream
  • string_decoder
  • timers
  • tty
  • url
  • util
  • vm
  • zlib

events, stream, url, path, and querystring are particularly useful in a browser environment.

querystring

在 builtins.js 里面:https://github.com/browserify/browserify/blob/4190ed509f46a17b2071af2c58cea505e41f43b4/lib/builtins.js#L21

exports.querystring = require.resolve('querystring-es3/');

依赖了 "querystring-es3": "~0.2.0", github 地址

babelify 又是什么?

Babel browserify transform

依赖了 browserify

"browserify": "^16.2.2",

babel-register

babel require hook

依赖了 "browserify": "^16.5.2"地址

babel-register/src/browser.js 文件如下:

// required to safely use babel/register within a browserify codebase
export default function register() {}
export function revert() {}

babel-register/test/browserify.js

import browserify from "browserify";
import path from "path";
import vm from "vm";

describe("browserify", function () {
  it("@babel/register may be used without breaking browserify", function (done) {
    const bundler = browserify(
      path.join(__dirname, "fixtures/browserify/register.js"),
    );

    bundler.bundle(function (err, bundle) {
      if (err) return done(err);
      expect(bundle.length).toBeTruthy();

      // ensure that the code runs without throwing an exception
      vm.runInNewContext("var global = this;\n" + bundle, {});
      done();
    });
  });
});

vue 原理

:class 合并

function concat (a, b) {
  return a ? b ? (a + ' ' + b) : a : (b || '')
}

webpack配置: optimization

optimization.runtimeChunk

  • 何为runtimeChunk
    • 简单理解:包含chunks 映射关系的 list
  • runtimeChunk配置
    • 三种配置:
      1.默认: false
      每个入口 chunk 中直接嵌入 runtime
      2.true <=> 'multiple' <=> { name: entrypoint => runtime~${entrypoint.name}}
      为每个只含有 runtime 的入口添加一个额外 chunk
      3.'single' <=> { name: 'runtime' }
      创建一个在所有生成 chunk 之间共享的运行时文件
    • 为什么要覆盖默认值false
      举例说明:把这个包含chunks映射关系的‘list’从output bundle(eg: app.111111.js)中提取出来, 生成runtime~app.222222.js。映射关系改变时app的hash值不变,runtime~app的hash值改变,用户可以充分利用浏览器缓存,不需重新请求app.111111.js, 只需重新请求runtime~app.333333.js

optimization.splitChunks

webpack 相关

配置项

bail

默认 false,官方地址
如果设置 true 的化,在第一个错误出现时抛出失败结果

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.