【若川视野 x 源码共读】第34期 | tdesign-vue 初始化组件

169 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第29天,点击查看活动详情

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

任务来源:juejin.cn/post/710910…

  • 新增组件: npm run init [组件名]
  • 删除组件:npm run init [组件名] del

源码链接:github1s.com/Tencent/tde…

新增组件

代码有这种例子,可以先看看 
* @example npm run init table
 *   /tdesign/tdesign-web-vue/test/unit/table/demo.test.js will be created.
 *   /tdesign/tdesign-web-vue/test/e2e/table/table.spec.js will be created.
 *   /tdesign/tdesign-web-vue/test/unit/table/index.test.js will be created.
 *   /tdesign/tdesign-web-vue/examples/table/demos/base.vue will be created.
 *   /tdesign/tdesign-web-vue/src/table/index.ts will be created.
 *   /tdesign/tdesign-web-vue/examples/table/table.md will be created.
 *   /tdesign/tdesign-web-vue/src/table/table.tsx will be created.

调试

// 之前是用tdesign-vue-next调试,现在改成tdesign-vue没事了

以前用 tdesign-vue-next的调试记录

git clone https://github.com/Tencent/tdesign-vue-next.git
pnpm i 
npm run init tab1 

> tdesign-vue-next@0.16.1 init
> git submodule init && git submodule update "tab1"

error: pathspec 'tab1' did not match any file(s) known to git

???

npm run start 时 因为clone子模块失败 一直跑不动,所以自己去下载子模块 src/common
下载:https://github.com/Tencent/tdesign-common/tree/2a83fe1940a68e607dec60f295a89a2d16027848
然后放进 src/common

npm run dev 跑起来

因为 npm run init tab1  和我们想的不一样,自行创建命令

"xiao": "node script/init/index.js",
这样就可以跑了    npm run xiao tab1

如果出现克隆时没有权限问题,可以修改 根目录的 .gitmodules 的 url 为 https 的

.gitmodules


[submodule "src/_common"]


  path = src/_common


  url = https://github.com/Tencent/tdesign-common.git

源码

const cwdPath = process.cwd(); 
function init() {
    // npm run init 组件名 del,	process.argv:[路径1,路径2,组件名 del]
    // 得到组件名 和 是否删除
  const [component, isDeleted] = process.argv.slice(2);
    // 没有组件名 提示错误并退出
  if (!component) {
    console.error('[组件名]必填 - Please enter new component name');
    process.exit(1);
  }
    // indexPath:tdesign/tdesign-web-vue/src/index.ts 
  const indexPath = path.resolve(cwdPath, 'src/index.ts');
    // 获取 组件 创建需要的文件列表们
  const toBeCreatedFiles = config.getToBeCreatedFiles(component);
    // 如果是删除,就删掉 这个组件相关的文件列表们,还有总的index.ts 去掉他的导入
  if (isDeleted === 'del') {
    deleteComponent(toBeCreatedFiles, component);
    deleteComponentFromIndex(component, indexPath);
  } else {
      // 添加,创建 文件列表们 的 目录和文件,总的index.ts 导入这个组件
    addComponent(toBeCreatedFiles, component);
    insertComponentToIndex(component, indexPath);
  }
}

init();

config.getToBeCreatedFiles(component)

返回 对象,key是目录,value是文件列表

function getToBeCreatedFiles(component) {
  // keys are directories, values are files.
  // desc - directory description
  // files - will be created
  // dirDeletable - if this directory can be deleted.
  return {
    [`src/${component}`]: {
      desc: 'component source code',
      files: [
        {
          file: 'index.ts',
          template: 'index.ts.tpl',
        },
        {
          file: `${component}.tsx`,
          template: 'component.tsx.tpl',
        },
      ],
    },
    [`examples/${component}`]: {
      desc: 'component API',
      files: [
        {
          file: `${component}.md`,
          template: 'component.md.tpl',
        },
      ],
    },
    [`examples/${component}/demos`]: {
      desc: 'component demo code',
      files: [
        {
          file: 'base.vue',
          template: 'base.demo.tpl',
        },
      ],
    },
    [`test/unit/${component}`]: {
      desc: 'unit test',
      files: [
        {
          file: 'index.test.js',
          template: 'index.test.tpl',
        },
        {
          file: 'demo.test.js',
          template: 'demo.test.tpl',
        },
      ],
    },
    [`test/e2e/${component}`]: {
      desc: 'e2e test',
      files: [`${component}.spec.js`],
    },
  };
}

module.exports = {
  getToBeCreatedFiles,
};

添加文件

function addComponent(toBeCreatedFiles, component) {
  // At first, we need to create directories for components.
    // 遍历要创建的文件目录 {[`src/${component}`]:{},。。。}
  Object.keys(toBeCreatedFiles).forEach((dir) => {
      // 组建 路径 ...src/tab1
    const _d = path.resolve(cwdPath, dir);
    fs.mkdir(_d, { recursive: true }, (err) => {
        // 包含错误信息的回调函数
      if (err) {
        utils.log(err, 'error');
        return;
      }
      console.log(`${_d} directory has been created successfully!`);
      // Then, we create files for components.
      const contents = toBeCreatedFiles[dir];
      contents.files.forEach((item) => {
          // 有些里面是有template和file的对象,有些只有个字符串
        if (typeof item === 'object') {
          if (item.template) {
            outputFileWithTemplate(item, component, contents.desc, _d);
          }
        } else {
            // 字符串的这个 直接创建 空文件
          const _f = path.resolve(_d, item);
          createFile(_f, '', contents.desc);
        }
      });
    });
  });
}
function outputFileWithTemplate(item, component, desc, _d) {
    // 拿模板文件信息
  const tplPath = path.resolve(__dirname, `./tpl/${item.template}`);
  let data = fs.readFileSync(tplPath).toString();
  const compiled = _.template(data);
    // 用loadsh 的 _.template 方法去把 组件名 塞进去。
  data = compiled({
    component,
    upperComponent: getFirstLetterUpper(component),
  });
    // 拼文件路径
  const _f = path.resolve(_d, item.file);
    // 创建文件
  createFile(_f, data, desc);
}
// 首字母大写
function getFirstLetterUpper(a) {
  return a[0].toUpperCase() + a.slice(1);
}
function createFile(path, data = '', desc) {
    // 创建成功了 通知一下。
  fs.writeFile(path, data, (err) => {
    if (err) {
      utils.log(err, 'error');
    } else {
      utils.log(`> ${desc}\n${path} file has been created successfully!`, 'success');
    }
  });
}

引入

// indexPath  终端运行路径/src/index.ts
function insertComponentToIndex(component, indexPath) {
    // 组件名大写 
  const upper = getFirstLetterUpper(component);
  // last import line pattern
  const importPattern = /import.*?;(?=\n\n)/;
  // components pattern const components = 这玩意根本就么有
  const cmpPattern = /(?<=const components = {\n)[.|\s|\S]*?(?=};\n)/g;
  const importPath = getImportStr(upper, component); // 拼一段import组件的字符串
  const desc = '> insert component into index.ts';
    // 获取index.ts的数据
  let data = fs.readFileSync(indexPath).toString();
    // 已经引入过了
  if (data.match(new RegExp(importPath))) {
    utils.log(`there is already ${component} in /src/index.ts`, 'notice');
    return;
  }
  // insert component at last import and component lines.
    // 改index.ts的数据,import 后面拼一段 import新组件的字符串
    // const components 加 新组件进去,但是就没有const components 这个东西呀?
  data = data.replace(importPattern, (a) => `${a}\n${importPath}`).replace(cmpPattern, (a) => `${a}  ${upper},\n`);
    // 把数据写进去。其实就加了个 一段 import新组件的字符串 我感觉作用好少
  fs.writeFile(indexPath, data, (err) => {
    if (err) {
      utils.log(err, 'error');
    } else {
      utils.log(`${desc}\n${component} has been inserted into /src/index.ts`, 'success');
    }
  });
}
// 拼一段import组件的字符串
function getImportStr(upper, component) {
  return `import ${upper} from './${component}';`;
}

删除文件

function getSnapshotFiles(component) {
  return {
    [`test/unit/${component}/__snapshots__/`]: {
      desc: 'snapshot test',
      files: ['index.test.js.snap', 'demo.test.js.snap'],
    },
  };
}

function deleteComponent(toBeCreatedFiles, component) {
  const snapShotFiles = getSnapshotFiles(component);
  const files = Object.assign(toBeCreatedFiles, snapShotFiles);
  Object.keys(files).forEach((dir) => {
    
    。。。// 删除这个目录 [`cwd()/src/${component}`]
      deleteFolderRecursive(dir);
    
  });
  utils.log('All radio files have been removed.', 'success');
}

function deleteFolderRecursive(path) {
    // 目录是否存在
  if (fs.existsSync(path)) {
      // 读取目录下的文件们
    fs.readdirSync(path).forEach((file) => {
      const current = `${path}/${file}`;
        // 如果是目录,递归
      if (fs.statSync(current).isDirectory()) {
        deleteFolderRecursive(current);
      } else {
          //同步删除文件 
        fs.unlinkSync(current);
      }
    });
      //用于同步删除给定路径下的目录 返回值为null 或 undefined则表示删除成功,否则将抛出异常。
    fs.rmdirSync(path);
  }
}

删除引入

function deleteComponentFromIndex(component, indexPath) {
  const upper = getFirstLetterUpper(component);
  const importStr = `${getImportStr(upper, component)}\n`;
  let data = fs.readFileSync(indexPath).toString();
    // 把匹配的import tab1 。。  给替换掉
  data = data.replace(new RegExp(importStr), () => '').replace(new RegExp(`  ${upper},\n`), '');
  fs.writeFile(indexPath, data, (err) => {
    if (err) {
      utils.log(err, 'error');
    } else {
      utils.log(`${component} has been removed from /src/index.ts`, 'success');
    }
  });
}

小疑问

添加插入模块的时候:

  • const components这东西全局都没搜到,导入组件的时候干嘛要匹配这个
  • 这个引入 只是加了 import tab1 。。 ,没有显示到页面上,就这吗?

新鲜事

tdesign-vue-next库中,发现 initgit submodule init && git submodule update 让我知道一个仓库里可以装其他子库,有点意思

  • 如果出现克隆时没有权限问题,可以修改 根目录的 .gitmodules 的 url 为 https 的
[submodule "src/_common"]
	path = src/_common
	url = https://github.com/Tencent/tdesign-common.git

----渲染模板时,用loadsh _.template 方法

// 例子
// 通过分隔符创建编译模板然后通过变量进行替换
var compiled = _.template('hello <%= user %>!');\
compiled({ 'user': 'world' });   

------------
// 实际
// 拿模板文件信息
  const tplPath = path.resolve(__dirname, `./tpl/${item.template}`);
  let data = fs.readFileSync(tplPath).toString();
  const compiled = _.template(data);   
// 用loadsh 的 _.template 方法去把 组件名 塞进去。
  data = compiled({
    component,
    upperComponent: getFirstLetterUpper(component),
  });

总结

1.要有个创建组件对应的文件列表,还有相应的模板,创建时,遍历创建。

2.不只是创建文件,还需要导入