kahirokunn
twitter: https://twitter.com/kahirokunn
qiita: https://qiita.com/kahirokunn
Vue.jsとReactが大好きで毎日書いてます.
最近の日課はVue 3 エコシステムからStable releaseがされてないか確認すること
Vueのエコシステムの現時点の状況
[@vue/cli](https://www.npmjs.com/package/@vue/cli)
vue-cliでは既にvue2,vue3どちらでプロジェクトを作成するか選べる
[vue-loader@16.0.0-rc.1](https://www.npmjs.com/package/vue-loader)
[vuex@4.0.0-rc.1](https://www.npmjs.com/package/vuex)
[vue-router@4.0.0-rc.3](https://www.npmjs.com/package/vue-router)
[vee-validate@4.0.0-beta.19](https://www.npmjs.com/package/vee-validate)
[vue-apollo-composable@4.0.0-alpha.12](https://www.npmjs.com/package/@vue/apollo-composable)
[buefy](https://www.npmjs.com/package/buefy)
buefyは無償のメンテナで構成されており、その為Vue3対応に対して積極的に進められておらず、Vue3対応プロジェクト自体も開始できていない.
Issue: https://github.com/buefy/buefy/issues/2505
[vuetify](https://www.npmjs.com/package/vuetify)
VuetifyのロードマップではVuetify 3.0でVue3対応する予定です.
https://vuetifyjs.com/en/introduction/roadmap
[nuxt](https://github.com/nuxt/nuxt.js/issues/5708#issuecomment-668555312)
現在進行中
[storybook](https://www.npmjs.com/package/storybook)
StorybookのVue3対応は、Vueのエコシステムからrcが取れてから進みそうな気配があります.
rcが取れれば立候補者が恐らく登場し、v6.2.0から使えるようになりそうな気がしています.
※2020年11月15日時点 Vue3対応待ちライブラリと時点でリリースされているバージョン番号 バージョン番号がないのは、Vue3対応がいつされるかわからないものです.
[@vue/cli](https://www.npmjs.com/package/@vue/cli)
vue-cliでは既にvue2,vue3どちらでプロジェクトを作成するか選べる
[vue-loader@16.0.0-rc.1](https://www.npmjs.com/package/vue-loader)
[vuex@4.0.0-rc.1](https://www.npmjs.com/package/vuex)
[vue-router@4.0.0-rc.3](https://www.npmjs.com/package/vue-router)
※2020年11月15日時点
時点でリリースされている公式のエコシステムをいくつかピックアップ
[vue-apollo-composable@4.0.0-alpha.12](https://www.npmjs.com/package/@vue/apollo-composable)
[buefy](https://www.npmjs.com/package/buefy)
buefyは無償のメンテナで構成されており、その為Vue3対応に対して積極的に進められておらず、Vue3対応プロジェクト自体も開始できていない.
Issue: https://github.com/buefy/buefy/issues/2505
[vuetify](https://www.npmjs.com/package/vuetify)
VuetifyのロードマップではVuetify 3.0でVue3対応する予定です.
https://vuetifyjs.com/en/introduction/roadmap
[nuxt](https://github.com/nuxt/nuxt.js/issues/5708#issuecomment-668555312)
現在進行中
[storybook](https://www.npmjs.com/package/storybook)
StorybookのVue3対応は、Vueのエコシステムからrcが取れてから進みそうな気配があります.
rcが取れれば立候補者が恐らく登場し、v6.2.0から使えるようになりそうな気がしています.
※2020年11月15日時点 時点でリリースされているサードパーティのエコシステムをいくつかピックアップ
StorybookはVue3対応してくれる人募集している
その他エコシステムもそういう方を募集している
react-router-route-generatorを作った話
諸事情により最近Reactを書いているのだが、auto routingか同等の物がReactで欲しかった.
でないと、創作意欲が凝らされた最高の戦闘力を持つpage達が誕生してしまう.
機械的に管理できるレールを引きたかった.
Next.jsとかにはこの手の仕組が搭載されているが、単体でそれだけをしてくれるライブラリが見当たらなかった.
なので、作成した.
仕様は、Next.jsのpagesと同じものを与えると、その通りにルーティングします.
もしサポートされてない機能で欲しいものがあったらIssue立ててくれたら素早く対応すると思います.
また、useParams hooks等で利用すると便利な型も吐いてくれます.
import React from 'react';
import { Route } from 'react-router';
import loadable from '@loadable/component';
export type UseParamsType = {
['/users/[userId]']: { ['userId']: string };
};
export const RouteConfig = {
['/users/:userId']: loadable(() => import('@/pages/users/[userId]/index.tsx')),
['/users']: loadable(() => import('@/pages/users/index.tsx')),
['/']: loadable(() => import('@/pages/index.tsx')),
};
export default () => (
<>
<Route path="/users/:userId" component={RouteConfig['/users/:userId']} exact />
<Route path="/users" component={RouteConfig['/users']} exact />
<Route path="/" component={RouteConfig['/']} exact />
</>
);
$ npm install -D react-router-route-generator
$ npx generate-routes
簡単に使える
大体Next.jsのルールを満たすように正規表現で実装している.
詳しく聞きたい場合は、個別に聞いてくれると幸いです!
import glob from 'glob';
import compareFunc from 'compare-func';
function assertsHasValue<T>(
value: T,
errorMessage: string,
): asserts value is Exclude<T, null | undefined | void> {
if (value === null || value === undefined) {
throw new Error(errorMessage);
}
}
function globSync(
patterns: string | string[],
ignorePatterns: string[],
): string[] {
if (typeof patterns === 'string') {
patterns = [patterns];
}
return patterns.reduce(
(acc, pattern) =>
acc.concat(glob.sync(pattern, { nodir: true, ignore: ignorePatterns })),
[] as string[],
);
}
type Slug = {
name: string;
isRest: boolean;
};
type Slugs = Slug[];
type MetaData = {
component: string;
urlPath: string;
path: string;
slugs?: Slugs;
isLastOptional: boolean;
};
// 拡張子を取り除く
// .replace(/^(.*)\.(js|jsx|ts|tsx)$/, '$1')
function genRouteMetaData(componentPath: string, prefetch: boolean): MetaData {
let urlPath = componentPath
// 末尾の/index.{js,jsx,ts,tsx} を消す
.replace(/^(.*)\/index\.(js|jsx|ts|tsx)$/, '$1')
// 先頭の@/pagesを取り除く
.replace(/^@\/pages(.*)$/, '$1');
const isLastOptional = /^(.*)\.(js|jsx|ts|tsx)$/.test(urlPath);
if (isLastOptional) {
urlPath = urlPath.replace(/^(.*)\.(js|jsx|ts|tsx)$/, '$1');
}
return _calcRouteMetaData({
path: urlPath,
isLastOptional,
component: `() => import(${
prefetch ? '/* webpackPrefetch: true */' : ''
} '${componentPath}')`
// 拡張子を取り除く
.replace(/^(.*)\.(js|jsx|ts|tsx)$/, '$1'),
urlPath: urlPath === '' ? '/' : urlPath,
});
}
// hoge/fuga/[piyo]/[piyopiyo] => hoge/fuga/:piyo/:piyopiyo
function _calcRouteMetaData(metaData: MetaData): MetaData {
const result = /^(.*)\[(.*?)\](.*)$/.exec(metaData.urlPath);
if (!result) {
if (metaData.isLastOptional) {
const items = metaData.urlPath.split('/');
items[items.length - 1] = `${items[items.length - 1]}?`;
metaData = {
...metaData,
urlPath: items.join('/'),
};
}
return metaData;
}
result.reverse();
let isRest = false;
const slug = /^\.\.\.(.*)$/.exec(result[1]);
if (slug) {
isRest = true;
result[1] = slug[1];
}
return _calcRouteMetaData({
...metaData,
urlPath: isRest
? `${result[2]}:${result[1]}(.*)`
: `${result[2]}:${result[1]}${result[0]}`,
slugs: [
...(metaData.slugs ?? []),
{
name: result[1],
isRest,
},
],
});
}
function route2RouteConfig(route: MetaData, wrap: string) {
return `
['${route.urlPath}']: ${wrap.replace('$1', route.component)}
`;
}
function route2RouteComponent(route: MetaData) {
return `
<Route path='${route.urlPath}' component={RouteConfig["${route.urlPath}"]} exact />
`;
}
type GenCodeInput = Partial<{
sourceHead: string;
wrap: string;
targetDir: string;
ignorePatterns: string[];
prefetch: boolean;
}>;
export function generate(params: GenCodeInput) {
const {
sourceHead = "import React from 'react'; import { Route } from 'react-router'; import loadable from '@loadable/component';",
wrap = 'loadable($1)',
targetDir = 'src/pages',
ignorePatterns = [],
prefetch = false,
} = params;
const routes = globSync(`${targetDir}/**/*`, ignorePatterns)
.map((filePath) => {
const result = /^src\/(.*)$/.exec(filePath);
assertsHasValue(result, "There can't be an error here.");
return `@/${result[1]}`;
})
.map((componentPath) => genRouteMetaData(componentPath, prefetch))
.sort(compareFunc('urlPath'))
.reverse();
const typeCode = `export type UseParamsType = {
${routes
.filter((route) => route.slugs)
.map(
(route) =>
`['${route.path}']: { ${(route.slugs as Slugs)
.map((slug) => `['${slug.name}']: string`)
.join(';')} }`,
)
.join(';')}
}`;
const routesCode = `
${sourceHead}
${typeCode}
export const RouteConfig = {
${routes.map((route) => route2RouteConfig(route, wrap))}
}
export default () => (
<>
${routes.map((route) => route2RouteComponent(route)).join('')}
</>
)
`;
return {
routesCode,
};
}