前言

书接上回 vee-cli脚手架实践(中)

上回主要介绍了create.js脚本的模板选择与拉取,本篇旨在讲述选择对应模板后编译以及最后的npm发包

模板编译

依赖包

[包目录结构]

  • metalsmith (用于遍历文件夹,判断是否需要进行模板渲染)
  • consolidate (统一所有的模板引擎)

[目录描述] 对于有模板引擎渲染的仓库,一般会有一个ask.js,

module.exports = [    {      type: 'confirm',      name: 'private',      message: 'ths resgistery is private?',    },    {      type: 'input',      name: 'author',      message: 'author?',    },    {      type: 'input',      name: 'description',      message: 'description?',    },    {      type: 'input',      name: 'license',      message: 'license?',    },  ]

与用户进行命令行交互后,将对应的内容动态注入到模板中,这里常用的模板引擎有ejs、handlebars等,consolidate将这里用到的引擎进行了统一,可以自由选择

逻辑代码

// 判断是否存在ask.js文件if(!fs.existsSync(path.join(result, 'ask.js'))) {    // 直接下载    await ncpPro(result, path.resolve(projectName));} else {    // 模板渲染后再拷贝    await new Promise((resolve,reject) => {      MetalSmith(__dirname)        .source(result)        .destination(path.resolve(projectName))        .use(async (files, metal, done) => {            const a = require(path.join(result, 'ask.js'));            const r = await Inquirer.prompt(a);            const m = metal.metadata();            Object.assign(m, r);            delete files['ask.js'];            done()        })        .use((files, metal, done) => {            const meta = metal.metadata();            Object.keys(files).forEach(async (file) => {                let c = files[file].contents.toString();                // 只有js和json文件才去做处理                if(file.includes('js') || file.includes('json')) {                    // 判断是否是模板 可用正则匹配                    if(c.includes('<%')) {                        c = await renderPro(c, meta);                        files[file].contents = Buffer.from(c);                    }                }            })            done()        })        .build((err) => {            if(err) {                reject()            } else {                resolve()            }        })    })}

这里主要是对之前直接down仓库内容复制做了扩展,判断是否需要进行模板编译,也就是用户是否需要再次输入内容,动态的注入到拉取的模板中,这里还可以选择更多的其他配置,但大致原理基本一致,具体详细的可以参看vue-cli源码,其中对其他部分也做了更为详尽的扩展

相关包源码分析

metalsmith

var assert = require('assert')var clone = require('clone')var fs = require('co-fs-extra')var matter = require('gray-matter')var Mode = require('stat-mode')var path = require('path')var readdir = require('recursive-readdir')var rm = require('rimraf')var thunkify = require('thunkify')var unyield = require('unyield')var utf8 = require('is-utf8')var Ware = require('ware')readdir = thunkify(readdir)rm = thunkify(rm)var isBoolean = function(b) {return typeof b === 'boolean'}var isNumber  = function(n) {return typeof n === 'number' && !Number.isNaN(n)}var isObject  = function(o) {return o !== null && typeof o === 'object'}var isString  = function(s) {return typeof s === 'string'}module.exports = Metalsmithfunction Metalsmith(directory){  if (!(this instanceof Metalsmith)) return new Metalsmith(directory)  assert(directory, 'You must pass a working directory path.')  this.plugins = []  this.ignores = []  this.directory(directory)  this.metadata({})  this.source('src')  this.destination('build')  this.concurrency(Infinity)  this.clean(true)  this.frontmatter(true)}Metalsmith.prototype.use = function(plugin){  this.plugins.push(plugin)  return this}Metalsmith.prototype.directory = function(directory){  if (!arguments.length) return path.resolve(this._directory)  assert(isString(directory), 'You must pass a directory path string.')  this._directory = directory  return this}Metalsmith.prototype.metadata = function(metadata){  if (!arguments.length) return this._metadata  assert(isObject(metadata), 'You must pass a metadata object.')  this._metadata = clone(metadata)  return this}Metalsmith.prototype.source = function(path){  if (!arguments.length) return this.path(this._source)  assert(isString(path), 'You must pass a source path string.')  this._source = path  return this}Metalsmith.prototype.destination = function(path){  if (!arguments.length) return this.path(this._destination)  assert(isString(path), 'You must pass a destination path string.')  this._destination = path  return this}Metalsmith.prototype.concurrency = function(max){  if (!arguments.length) return this._concurrency  assert(isNumber(max), 'You must pass a number for concurrency.')  this._concurrency = max  return this}Metalsmith.prototype.clean = function(clean){  if (!arguments.length) return this._clean  assert(isBoolean(clean), 'You must pass a boolean.')  this._clean = clean  return this}Metalsmith.prototype.frontmatter = function(frontmatter){  if (!arguments.length) return this._frontmatter  assert(isBoolean(frontmatter), 'You must pass a boolean.')  this._frontmatter = frontmatter  return this}Metalsmith.prototype.ignore = function(files){  if (!arguments.length) return this.ignores.slice()  this.ignores = this.ignores.concat(files)  return this}Metalsmith.prototype.path = function(){  var paths = [].slice.call(arguments)  paths.unshift(this.directory())  return path.resolve.apply(path, paths)}Metalsmith.prototype.build = unyield(function*(){  var clean = this.clean()  var dest = this.destination()  if (clean) yield rm(path.join(dest, '*'), { glob: { dot: true } })  var files = yield this.process()  yield this.write(files)  return files})Metalsmith.prototype.process = unyield(function*(){  var files = yield this.read()  files = yield this.run(files)  return files})Metalsmith.prototype.run = unyield(function*(files, plugins){  var ware = new Ware(plugins || this.plugins)  var run = thunkify(ware.run.bind(ware))  var res = yield run(files, this)  return res[0]})Metalsmith.prototype.read = unyield(function*(dir){  dir = dir || this.source()  var read = this.readFile.bind(this)  var concurrency = this.concurrency()  var ignores = this.ignores || null  var paths = yield readdir(dir, ignores)  var files = []  var complete = 0  var batch  while (complete < paths.length) {    batch = paths.slice(complete, complete + concurrency)    batch = yield batch.map(read)    files = files.concat(batch)    complete += concurrency  }  return paths.reduce(memoizer, {})  function memoizer(memo, file, i) {    file = path.relative(dir, file)    memo[file] = files[i]    return memo  }})Metalsmith.prototype.readFile = unyield(function*(file){  var src = this.source()  var ret = {}  if (!path.isAbsolute(file)) file = path.resolve(src, file)  try {    var frontmatter = this.frontmatter()    var stats = yield fs.stat(file)    var buffer = yield fs.readFile(file)    var parsed    if (frontmatter && utf8(buffer)) {      try {        parsed = matter(buffer.toString())      } catch (e) {        var err = new Error('Invalid frontmatter in the file at: ' + file)        err.code = 'invalid_frontmatter'        throw err      }      ret = parsed.data      ret.contents = (Buffer.hasOwnProperty('from'))        ? Buffer.from(parsed.content)         : new Buffer(parsed.content)    } else {      ret.contents = buffer    }    ret.mode = Mode(stats).toOctal()    ret.stats = stats  } catch (e) {    if (e.code == 'invalid_frontmatter') throw e    e.message = 'Failed to read the file at: ' + file + '\n\n' + e.message    e.code = 'failed_read'    throw e  }  return ret})Metalsmith.prototype.write = unyield(function*(files, dir){  dir = dir || this.destination()  var write = this.writeFile.bind(this)  var concurrency = this.concurrency()  var keys = Object.keys(files)  var complete = 0  var batch  while (complete < keys.length) {    batch = keys.slice(complete, complete + concurrency)    yield batch.map(writer)    complete += concurrency  }  function writer(key){    var file = path.resolve(dir, key)    return write(file, files[key])  }})Metalsmith.prototype.writeFile = unyield(function*(file, data){  var dest = this.destination()  if (!path.isAbsolute(file)) file = path.resolve(dest, file)  try {    yield fs.outputFile(file, data.contents)    if (data.mode) yield fs.chmod(file, data.mode)  } catch (e) {    e.message = 'Failed to write the file at: ' + file + '\n\n' + e.message    throw e  }})

metalsmith用的是挂在原型上的写法,通过插件的链式传递方法进行数据的透传,属于原型设计模式的应用,对于js来说原型模式是天生存在的,因而对于希望通过链式传递且写法且变量不多的小型库而言,这种方式不失为一种好的方法,对链式调用有兴趣的同学可以研究下jQuery源码及koa源码,虽然大型库组织不是一种模式的展现,但是其中小部分还是有异曲同工的地方的,对于链式调用的实现方法也可以有一个横向的扩展和对比

consolidate

consolidate主要是对不同模板引擎的选择分发,这里挑选了最核心的几个功能函数

function cache(options, compiled) {      if (compiled && options.filename && options.cache) {    delete readCache[options.filename];    cacheStore[options.filename] = compiled;    return compiled;  }  if (options.filename && options.cache) {    return cacheStore[options.filename];  }  return compiled;}function read(path, options, cb) {  var str = readCache[path];  var cached = options.cache && str && typeof str === 'string';  if (cached) return cb(null, str);  fs.readFile(path, 'utf8', function(err, str) {    if (err) return cb(err);    str = str.replace(/^\uFEFF/, '');    if (options.cache) readCache[path] = str;    cb(null, str);  });}function readPartials(path, options, cb) {  if (!options.partials) return cb();  var keys = Object.keys(options.partials);  var partials = {};  function next(index) {    if (index === keys.length) return cb(null, partials);    var key = keys[index];    var partialPath = options.partials[key];    if (partialPath === undefined || partialPath === null || partialPath === false) {      return next(++index);    }    var file;    if (isAbsolute(partialPath)) {      if (extname(partialPath) !== '') {        file = partialPath;      } else {        file = join(partialPath + extname(path));      }    } else {      file = join(dirname(path), partialPath + extname(path));    }    read(file, options, function(err, str) {      if (err) return cb(err);      partials[key] = str;      next(++index);    });  }  next(0);}function fromStringRenderer(name) {  return function(path, options, cb) {    options.filename = path;    return promisify(cb, function(cb) {      readPartials(path, options, function(err, partials) {        var extend = (requires.extend || (requires.extend = require('util')._extend));        var opts = extend({}, options);        opts.partials = partials;        if (err) return cb(err);        if (cache(opts)) {          exports[name].render('', opts, cb);        } else {          read(path, opts, function(err, str) {            if (err) return cb(err);            exports[name].render(str, opts, cb);          });        }      });    });  };}

consolidate这个库也是tj大佬写的,其主要思路是通过读取[read]对应文件里的字符[readPartials]获取到需要的字符后对字符进行查找对应名称[fromStringRenderer]的渲染,其中读取过程做了[cache]优化,剩下的就是对对应的模板渲染引擎的分发,从而做到了汇聚分发的效果,整体思路还是很明确的,另外多说一句,tj大佬似乎对类生成器函数处理有种蜜汁喜爱,各种库都有它的影子,对生成器方式处理感兴趣的同学,可以参考co库源码

发包

连接npm

连接npm源(如果没有nrm,需要npm i nrm -g) => 填写npm官网的个人用户信息

发布到npm

对于整个npm发包等感兴趣的同学,可以参考npm文档,也可以参考这篇文章npm包的发布与删除

验证

搜索npmjs官网上,可以查找到,npm unlink后或换一台机器,可以npm i vee-cli进行包下载,这样一个脚手架的发包就完成了

总结

脚手架是前端工程化领域的基本项,个人认为掌握前端脚手架的开发是十分重要的,这三篇内容
vee-cli脚手架实践(上)
vee-cli脚手架实践(中)
vee-cli脚手架实践(下)
旨在提供一个大概思路及样板,目前只包含了

1、命令行;2、模板拉取;

,其相对于成熟的脚手架如vue-cli、create-react-app、@angular/cli等来说,还有很多很多工作要做,包括

3、本地服务;4、打包构建;5、集成部署;6、周边其他

等都还需要完善,想要在工程化领域有所建树的同学,不妨在这几个方面多下下功夫

vee-cli源代码

参考

  • metalsmith.js源码
  • consolidate.js源码
  • co.js源码
  • jquery.js源码
  • koa.js源码
  • npm包可以修改名字吗?
  • 用Angular脚手架搭建项目
  • React(脚手架)
  • co 函数库的含义和用法
  • 有哪些好用的前端模板引擎?