xinpianchang / fe-weekly Goto Github PK
View Code? Open in Web Editor NEWweekly for fe-team
weekly for fe-team
function concat (a, b) {
return a ? b ? (a + ' ' + b) : a : (b || '')
}
runtime~${entrypoint.name}
}在浏览器一帧的剩余空闲时间内执行优先度相对较低的任务,一般按先进先调用的顺序执行
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 里面进行
不建议
地址: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
出现了 let 定义变量
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
不再向 document 附加事件处理器,会将事件处理器附加到渲染 React 树的根 DOM 容器中
不再冒泡,
在底层切换为原生的 focusin
和 focusout
,总是冒泡的
demo 地址
官方文档:https://zh-hans.reactjs.org/docs/legacy-event-pooling.html
demo 地址如下
官方文档地址:
浏览器原生事件的跨浏览器包装器,从 17 开始 e.persist() 不再生效,不再放入事件池
browser-side require() the node.js way. Browserify lets you require('modules') in the browser by bundling up all of your dependencies.
官方的 handbook 地址
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:
events, stream, url, path, and querystring are particularly useful in a browser environment.
在 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 地址
Babel browserify transform
依赖了 browserify
"browserify": "^16.2.2",
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();
});
});
});
第一个问题,路径里面有 []
的如何处理
isDynamicRoute
源码地址const TEST_ROUTE = /\/\[[^/]+?\](?=\/|$)/
export function isDynamicRoute(route: string): boolean {
return TEST_ROUTE.test(route)
}
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 |
@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
官方配置项地址说明:如下
string 类型,比如 “1 0 * * *”
a cron pattern to restart your app. Application must be running for cron feature to work
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…
因为我们大部分的业务应用场景是 Custom Server
,所以会关注到这个配置项,先看一下文档:
默认情况下,next 会自动在服务端给 pages
目录下的文件生成对应的访问地址,但是:
disables filename routes from SSR; client-side routing may still access those paths.
module.exports = {
useFileSystemPublicRoutes: false
}
使用 next/router
的 beforePopState
router.beforePopState(cb)
参数 cb 返回 false
,就不会执行 popstate
具体的参数如下:
router.beforePopState(({ url, as, options }) => {
})
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'
}
]
}
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
}
}
}
官方说明:地址
/[^\x00-\xff]/g
# 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
方式一 preset-env + @babel/polyfill
设置preset-env的options.useBuiltIns(false | 'entry' | 'usage' 默认为false)
所以,这种方式的配置为:
安装@babel/preset-env和@babel/polyfill
入口文件(useBuiltIns使用false或entry时)
import "@babel/polyfill"
babel配置文件
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "entry" //(| "usage" | false )
}
]
]
}
方式二 preset-env + @babel/plugin-transform-runtime + @babel/runtime-corejs2
preset-env不需设置useBuiltIns, 但需要设置@babel/plugin-transform-runtime的options.corejs(boolean or number,默认值是false)
所以,这种方式的配置为:
安装@babel/preset-env, @babel/runtime-corejs2和@babel/plugin-transform-runtime
入口文件不需做任何引入
babel配置文件
{
"presets": ["@babel/preset-env"],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 2
}
]
]
}
support instance methods!!
corejs3
import "core-js/stable";
import "regenerator-runtime/runtime";
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "entry" //(| "usage" | false )
"corejs": {
"version": 3, // 使用core-js@3
"proposals": true,
}
}
]
]
}
参考:
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)) {
}
}
}
相比本地开发,我们会开启 HMR:
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
文档地址:如下
文档参考:地址
hot module replacement 模块热替换 - 会在应用程序运行过程中,替换、添加或删除 模块,而无需重新加载整个页面。主要是通过以下几种方式,来显著加快开发速度:
文档参考:地址
模块热替换是否启用,并给进程提供一个接口
接受(accept)给定 依赖模块(dependencies) 的更新,并触发一个 回调函数 来响应更新。
语法:
module.hot.accept(
dependencies, // 可以是一个字符串或字符串数组
callback // 用于在模块更新后触发的函数
)
在 .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
MDN 文档:地址
服务器推送的一个网络事件接口,一个EventSource实例会对HTTP服务开启一个持久化的连接,以text/event-stream 格式发送事件, 会一直保持开启直到被要求关闭。
一定要有<Nuxt />组件, 用于"actually include the page component"
default.vue
默认layout, 如果page没有指定layout则使用default.vue
custom layout
所有/layouts目录下的文件(top-level)都是custom layout
可用string / function
使用function来指定layout时, 可根据上下文context指定不同layout
error.vue
虽在layouts目录下,但不是一个layout,而是一个page,所以无<Nuxt />组件,且仍可以为这个error.vue指定layout
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>
.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
},
the open-source Headless CMS (opens new window)developers love.
这个是创建表时默认结尾会加个s,在高级设置中可以自己设置表名
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 |
官网地址
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;
}
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"}
先看一下我们的配置:
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 吗?
module: {
rules: [
{
test: '/\.ts|\.js|\.mjs$/',
use: [
{
loader: 'babel-loader',
options: {
cacheDirectory: true
},
}
]
}
]
}
主要是针对 8.1.0
,源码地址
后面会提到具体使用了什么方法
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-utils
的 getOptions
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",
})
}
源码如下
官方对于 options 里面的配置项也有说明:地址
整体是一个对象,包含属性:
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.
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.
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.
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 来自 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,
);
},
);
export default function* loadPrivatePartialConfig(
inputOpts: mixed,
): Handler<PrivPartialConfig | null> {
}
返回的 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;
}
依赖了 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,
);
}
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;
}
依赖了 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);
}
export const ROOT_CONFIG_FILENAMES = [
"babel.config.js",
"babel.config.cjs",
"babel.config.mjs",
"babel.config.json",
];
也在babel-core/src/config/files/configuration.js
const RELATIVE_CONFIG_FILENAMES = [
".babelrc",
".babelrc.js",
".babelrc.cjs",
".babelrc.mjs",
".babelrc.json",
];
KeyboardEvent.keyCode === 229 判断IME, but keyCode:
应使用:
"compositionstart" "compositionupdate" "compositionend"
https://developer.mozilla.org/zh-CN/docs/Web/Events/compositionend
"compositionend" 与 "keydown"触发顺序问题(Safari特殊)
https://developer.squareup.com/blog/understanding-composition-browser-events/
代码地址: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)
简单示例:
定义一个
key
为name
,值是一个对象
const descriptor = {
name: { type: 'string', required: true }
}
官方支持:
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.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.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.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 }
}
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();
}
});
},
}
}
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;
The
required
rule property indicates that the field must exist on the source object being validated.
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
You can custom validate function for specified field:
const descriptor = {
field2: {
validator(rule, value, callback) {
return new Error(`${value} is not equal to 'test'.`);
},
}
}
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();
},
}
}
{
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
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,
}
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),
);
}
}
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 接收形参 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
自定义错误信息
const cn = {
required: '%s 必填',
};
const descriptor = { name: { type: 'string', required: true } };
const validator = new Schema(descriptor);
validator.messages(cn);
export function isEmptyObject(obj) {
return Object.keys(obj).length === 0;
}
依赖内部方法 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;
}
只考虑单独一个el-checkbox的情况,不考虑其在el-checkbox-group中的情况
<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并不对应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,只要不重新创建组件,都不会有作用。
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会使得model这个计算属性get得到的数据变化,input checkbox的v-model="model", 从而改变input的checked状态
created后不论如何修改checked,只要不重新创建组件,都不会有作用。
官方文档的解释:地址
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
<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
}
}
export default {
name: 'NuxtLink',
props: {
prefetch: {},
noPrefetch: {},
},
mounted () {
if (this.prefetch && !this.noPrefetch) {
this.handleId = requestIdleCallback(this.observe, { timeout: 2e3 })
}
}
}
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)
}
methods: {
observe () {
if (!observer) {
return
}
if (this.shouldPrefetch()) {
this.$el.__prefetch = this.prefetchLink.bind(this)
observer.observe(this.$el)
this.__observed = true
}
}
}
提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(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 () {
return this.getPrefetchComponents().length > 0
}
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)
}
先调用 canPrefetch
prefetchLink () {
if (!this.canPrefetch()) {
return
}
// ...
}
这里用到了 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
}
这里还调用了 cancelIdleCallback
beforeDestroy () {
cancelIdleCallback(this.handleId)
if (this.__observed) {
observer.unobserve(this.$el)
delete this.$el.__prefetch
}
}
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
}
IUAParser 来自于第三方工具包:ua-parser-js 它的定义文件:@types/ua-parser-js
declare namespace UAParser {
}
"@types/ua-parser-js": "0.7.33",
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
import { ParsedUrlQuery } from 'querystring'
export type GetServerSidePropsContext<Q extends ParsedUrlQuery = ParsedUrlQuery> = {
req: IncomingMessage
res: ServerResponse
params?: Q
query: ParsedUrlQuery
preview?: boolean
previewData?: any
}
还是先判断是否是动态路由
:
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
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
}
这里的方法来自 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) {
// ...
}
}
在 next-server.ts 有一个 private
的 findPageComponents
函数
接受 3 个参数:
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)
这里:
在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)
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> {
}
参数:
判断是否有 dynamicRoutes
,循环 dynamicRoutes
这里的 dynamicRoutes 为 { page: '/ssr/[id]', match: [Function] }
if (this.dynamicRoutes) {
for (const dynamicRoute of this.dynamicRoutes) {
// ...
}
}
const defaultConfig: { [key: string]: any } = {
useFileSystemPublicRoutes: true,
}
从 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)),
}))
}
这里的 dynamicRoute
来 match pathname
const params = dynamicRoute.match(pathname)
params { id: 'server-side-rendered2' }
目录结构:
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)
};
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' }] }] }]
官方给的文档:地址
地址:点击
核心代码:
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
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???)
});
},
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'事件的监听
}
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);
});
},
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-item在form中的添加和删除
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时触发'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销毁时触发'el.form.removeField'
beforeDestroy() {
this.dispatch('ElForm', 'el.form.removeField', [this]);
}
找到这个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;
},
返回父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中,用于以字符串的形式寻找相应属性)
},
返回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);
},
返回可以触发验证的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));
},
官方给的 DEMO地址
next start
启动的服务(start 内置的命令)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 点:
koa2-router
next-koa
解决为什么可以从 req.params 里面有动态参数?
import Router from 'koa2-router'
import { Context } from 'koa'
const router = new Router<any, Context>('www')
看一下 koa2-router 如何处理 url 里面有 :id
的
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
}
}
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
}
}
export type LoadComponentsReturnType = {
Components: React.ComponentType,
// ...
getServerSideProps?: GetServerSideProps
}
({
"./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
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.