第一章:本周导学
1-1 本周整体内容介绍和学习方法
- 重点:脚手架安装 项目/组件 功能开发。
- 技术栈:ejs模版渲染(项目模板安装)和glob文件筛选。
- 加餐:ejs源码解析、require源码解析。
第二章:脚手架安装模版功能架构设计
2-1 脚手架安装项目模板架构设计 installTemplate
2-2 脚手架组件初始化架构设计
与项目大体过程没有改变。 tiny change:
- 文本提示名称
- 项目名称format
- 组件需要填写描述信息
第三章 脚手架模板安装核心实现:ejs 库功能详解
3-1 ejs模板引擎的三种基本用法
ejs主要用于模版渲染的。jsp、php是之前模版渲染的代表。ejs的实现与jsp非常类似。
- ejs.compile(html,options)(data)
const ejs = require('ejs')
const path = require('path')
//第一种方法
const html ='<div><%= user.name%></div>'
const options = {}
const data ={
user:{
name:'liugezhou'
}
}
const template = ejs.compile(html,options) //// 返回一个用于解析html中模板的 function
const compileTemplate = template(data)
console.log(compileTemplate) //<div>liugezhou</div>
//第二种用法
const renderTemplate = ejs.render(html,data,options)
console.log(renderTemplate)
//第三种用法
const renderFile = ejs.renderFile(path.resolve(__dirname,'template.html'),data,options)
renderFile.then(file => console.log(file))
3-2 ejs模板不同标签用法详解
- <% : ‘脚本’标签,用于流程控制,无输出。
- <%= : 输出数据到模版(输出是转义Html标签)
- <%- : 输出非转义的数据到模版 :如果数据是liugehou ,那么输出的就是这样的格式。
- <%# : 注释标签,不执行、不输出内容,但是会占空间。
- <%_ : 删除前面空格空符
- -%>: 删除紧随其后的换行符
- _%>: 删除后面空格字符
3-3 ejs模板几种特殊用法
本节主要介绍ejs另外比较常用的三个辅助功能
- 包含: include
- 自定义分隔符: 我们上面默认使用的是%,我们只需要在options参数中定义 delimiter这个参数即可
- 自定义文件加载器: 在使用ejs.renderFile读取文件之前,可以使用ejs.fileLoader做一些操作
ejs.fileLoader = function(filePath){
const content = fs.readFileSync(filePath)
return '<div><%= user.copyright %></div>' content
}
3-4 glob用法小结
代码语言:javascript复制glob最早是出现在类Unix系统的命令中的,用来匹配文件路径。
const glob = require('glob')
glob('**/*.js',{ignore:['node_modules/**','webpack.config.js']},function(err,file){
console.log(file)
})
第四章:脚手架项目模板安装功能开发
4-1 引入项目模板类型和标准安装逻辑开发
本节代码较少,主要是梳理流程,上一大周写到了下载模版到本地缓存,本节接着上周进度: 接着便需要安装模版,新建了安装模版 installTemplate()方法,并对拿到模版的type进行判断, 若为normal,则执行安装标准模版方法:installNormalTemplate() 若为custom,则执行安装自定义模版方法:installCustomTemplate()
4-2 拷贝项目模板功能开发
代码语言:javascript复制async installNormalTemplate(){
//拷贝模板代码至当前目录
const spinner = spinnerStart('正在安装模板...')
await sleep()
try {
// 去缓存目录中拿template下的文件路径
const templatePath = path.resolve(this.templateNpm.cacheFilePath,'template')
//当前执行脚手架目录
const targetPath = process.cwd()
fse.ensureDirSync(templatePath)//确保使用前缓存生成目录存在,若不存在则创建
fse.ensureDirSync(targetPath) //确保当前脚手架安装目录存在,若不存在则创建
fse.copySync(templatePath,targetPath) //将缓存目录下文件copy至当前目录
} catch (error) {
throw error
} finally{
spinner.stop(true)
log.success('模板安装成功')
}
}
4-3 项目模板安装依赖和启动命令 | 4-4 白名单命令检测功能开发
在上一节,模板copy成功之后,紧接着:
代码语言:javascript复制//依赖安装
const { installCommand,startCommand } = this.templateInfo
//依赖安装
await this.execCommand(installCommand,'依赖过程安装失败!')
//启动命令执行
await this.execCommand(startCommand,'启动命令执行失败失败!')
代码语言:javascript复制const WHITE_COMMAND =['npm', 'cnpm']
async execCommand(command,errMsg){
let ret
if(command){
const cmdArray=command.split(' ')
const cmd = this.checkCommand(cmdArray[0])
if(!cmd){
throw new Error(errMsg)
}
const args = cmdArray.slice(1)
ret = await execAsync(cmd,args,{
stdio:'inherit',
cwd:process.cwd()
})
if(ret !== 0){//执行成功
throw new Error('依赖安装过程失败')
}
return ret
}
}
checkCommand(cmd){
if(WHITE_COMMAND.includes(cmd)){
return cmd
}
return null;
}
4-5 项目名称自动格式化功能开发
代码语言:javascript复制本节使用了kebab-case这个库,将手动填入的项目名称保存在projectInfo中,以供后续package.json中的ejs渲染使用。
//生成className
if(projectInfo.projectName){
projectInfo.name = projectInfo.projectName
projectInfo.className = require('kebab-case')(projectInfo.projectName).replace(/^-/,'');
}
if(projectInfo.projectVersion){
projectInfo.version = projectInfo.projectVersion
}
4-6 本章核心:ejs动态渲染项目模板
- 首先将vue2模版中package.json文件中的name以及version使用<%= className%>和<%= version%>替代,并发布新的版本至npm。
- commands/init模块安装 ejs和glob库。
- 核心代码如下(在4-4节中依赖安装前,ejs动态渲染)
async ejsRender(options){
const dir = process.cwd()
const projectInfo = this.projectInfo
return new Promise((resolve,reject)=>{
glob('**',{
cwd:dir,
ignore:options.ignore || '',
nodir:true //不输出文件夹,只输出文件
},(err,files) =>{
if(err){
reject(err)
}
Promise.all(files.map(file=>{
const filePath = path.join(dir,file)
return new Promise( (resolve1,reject1) => {
ejs.renderFile( filePath,projectInfo,{},(err,result) => {
if(err){
reject1(err)
}
fse.writeFileSync(filePath,result)
resolve1(result)
})
})
})).then(()=>{
resolve()
}).catch(err=>{
reject(err)
})
})
})
}
4-7 init命令直接传入项目名称功能支持
本节完成的是 对命令行中传入项目名称的一个支持 通过判断脚手架命令是否传入项目名称,对inquirer中的prompt进行动态push。
第五章 组件模板开发及脚手架组件初始化功能支持
5-1 慕课乐高组件库模板开发
维护组件库发布至npm,然后在mongodb数据库中进行配置。
5-2 项目和组件模板数据隔离 动态配置ejs ignore
这部分完整代码如下
代码语言:javascript复制 //1.选取创建项目或组件
const { type } = await inquirer.prompt({
type:'list',
name:'type',
message:'请选择初始化类型',
default:TYPE_PROJECT,
choices: [{
name: '项目',
value: TYPE_PROJECT,
}, {
name: '组件',
value: TYPE_COMPONENT,
}]
})
// 数据隔离核心代码
this.template = this.template.filter(template =>template.tag && template.tag.includes(type))
const title = type === TYPE_PROJECT ? '项目':'组件'
const projectNamePrompt = {
type:'input',
name:'projectName',
message:`请输入${title}的名称`,
default:'',
validate:function(v){
const done = this.async()
setTimeout(function(){
if(!isValidName(v)){
done(`请输入合法的${title}名称`)
return;
}
done(null,true)
}, 0);
},
filter:function(v){
return v
}
}
const projectPrompt = []
if (!isProjectNameValid) {
projectPrompt.push(projectNamePrompt);
}
projectPrompt.push({
type:'input',
name:'projectVersion',
default:'1.0.0',
message:`请输入${title}版本号`,
validate:function(v){
const done = this.async();
// Do async stuff
setTimeout(function() {
if (!(!!semver.valid(v))) {
done(`请输入合法的${title}版本号`);
return;
}
done(null, true);
}, 0);
},
filter:function(v){
if(semver.valid(v)){
return semver.valid(v)
} else {
return v
}
},
},{
type:'list',
name:'projectTemplate',
message:`请选择${title}模板`,
choices: this.createTemplateChoice()
})
5-3 获取组件信息功能开发
完整核心代码如下,添加了 descriptionPrompt
代码语言:javascript复制else if (type === TYPE_COMPONENT){
// 获取组件的基本信息
const descriptionPrompt = {
type:'input',
name:'componentDescription',
message:'请输入组件描述信息',
default:'',
validate:function(v){
const done = this.async()
setTimeout(() => {
if(!v){
done('请输入组件描述信息')
return
}
done(null,true)
}, 0);
}
}
projectPrompt.push(descriptionPrompt)
const component = await inquirer.prompt(projectPrompt)
projectInfo = {
...projectInfo,
type,
...component
}
}
……
if(projectInfo.componentDescription){
projectInfo.description = projectInfo.componentDescription
}
5-4 解决组件库初始化过程中各种工程问题
慕课乐高组件库,在发布到npm包时,安装出现问题,问题原因是 package.json中,需要将 “files”:[‘dist’] 这行代码去除,这是因为files这里限定了上传发布到npm后只有dist这个目录。
第六章 脚手架自定义初始化项目模板功能开发
6-1 自定义项目模板开发
- 发布自定义模版 liugezhou-cli-dev-template-custom-vue2
- mongodb中配置自定义模版数据。
6-2 自定义模板执行逻辑开发 6-3 自定义模板上线
代码语言:javascript复制async installCustomTemplate(){
//查询自定义模版的入口文件
if(await this.templateNpm.exists()){
const rootFile = this.templateNpm.getRootFilePath()
if(fs.existsSync(rootFile)){
log.verbose('开始执行自定义模板')
const templatePath = path.resolve(this.templateNpm.cacheFilePath, 'template');
const options = {
templateInfo: this.templateInfo,
projectInfo: this.projectInfo,
sourcePath: templatePath,
targetPath: process.cwd(),
};
const code = `require('${rootFile}')(${JSON.stringify(options)})`
await execAsync('node', ['-e', code], {stdio:'inherit',cwd: process.cwd()})
log.success('自定义模版安装成功')
}else{
throw new Error('自定义模板入口文件不存在')
}
}
}
第七章 本周加餐:ejs 库源码解析 —— 彻底搞懂模板动态渲染原理
7-1 ejs.compile执行流程分析
ejs模版渲染的思路值得我们学习,于是我们就开始了了ejs的源码的学习。
点击查看【processon】
代码语言:javascript复制本节内容较简单,我们打开webstore,从下面的代码开始调试(11行 打断点)
const ejs = require('ejs')
const html = '<div><%= user.name %></div>'
const options = {}
const data = {
user:{
name:'liugezhou'
}
}
const template = ejs.compile(html,options)
const compiletemplate = template(data)
代码语言:javascript复制//ejs.js
exports.compile = function compile(template, opts) {
var templ;
if (opts && opts.scope) { //我们的opts传进来的参数为空,暂不看此判断逻辑
……
}
templ = new Template(template, opts);
return templ.compile();
};
templ = new Template(template,opts) 我们继续进去源码,重要的有两点
- this.templateText = text
- this.regex = this.createRegex()
下节开始 templ.compile()
代码语言:javascript复制function Template(text, opts) {
opts = opts || {};
var options = {};
this.templateText = text; //⭐️⭐️⭐️
this.mode = null;
this.truncate = false;
this.currentLine = 1;
this.source = '';
options.client = opts.client || false;
options.escapeFunction = opts.escape || opts.escapeFunction || utils.escapeXML;
options.compileDebug = opts.compileDebug !== false;
options.debug = !!opts.debug;
options.filename = opts.filename;
options.openDelimiter = opts.openDelimiter || exports.openDelimiter || _DEFAULT_OPEN_DELIMITER;
options.closeDelimiter = opts.closeDelimiter || exports.closeDelimiter || _DEFAULT_CLOSE_DELIMITER;
options.delimiter = opts.delimiter || exports.delimiter || _DEFAULT_DELIMITER;
options.strict = opts.strict || false;
options.context = opts.context;
options.cache = opts.cache || false;
options.rmWhitespace = opts.rmWhitespace;
options.root = opts.root;
options.includer = opts.includer;
options.outputFunctionName = opts.outputFunctionName;
options.localsName = opts.localsName || exports.localsName || _DEFAULT_LOCALS_NAME;
options.views = opts.views;
options.async = opts.async;
options.destructuredLocals = opts.destructuredLocals;
options.legacyInclude = typeof opts.legacyInclude != 'undefined' ? !!opts.legacyInclude : true;
if (options.strict) {
options._with = false;
}
else {
options._with = typeof opts._with != 'undefined' ? opts._with : true;
}
this.opts = options;
this.regex = this.createRegex(); // ⭐️⭐️⭐️:该方法是对ejs标识符号%与开始结尾符号<>,进行定制化操作
}
7-2 深入讲解ejs编译原理
代码语言:javascript复制上一节我们看到了 return templet.compile()处,源代码如下
compile: function () {
var src;
var fn;
var opts = this.opts;
var prepended = '';
var appended = '';
var escapeFn = opts.escapeFunction;
var ctor;
var sanitizedFilename = opts.filename ? JSON.stringify(opts.filename) : 'undefined';
if (!this.source) {
this.generateSource(); //⭐️⭐️⭐️⭐️⭐️
prepended =
' var __output = "";n'
' function __append(s) { if (s !== undefined && s !== null) __output = s }n';
if (opts.outputFunctionName) {
prepended = ' var ' opts.outputFunctionName ' = __append;' 'n';
}
if (opts.destructuredLocals && opts.destructuredLocals.length) {
var destructuring = ' var __locals = (' opts.localsName ' || {}),n';
for (var i = 0; i < opts.destructuredLocals.length; i ) {
var name = opts.destructuredLocals[i];
if (i > 0) {
destructuring = ',n ';
}
destructuring = name ' = __locals.' name;
}
prepended = destructuring ';n';
}
if (opts._with !== false) {
prepended = ' with (' opts.localsName ' || {}) {' 'n';
appended = ' }' 'n';
}
appended = ' return __output;' 'n';
this.source = prepended this.source appended;
}
if (opts.compileDebug) {
src = 'var __line = 1' 'n'
' , __lines = ' JSON.stringify(this.templateText) 'n'
' , __filename = ' sanitizedFilename ';' 'n'
'try {' 'n'
this.source
'} catch (e) {' 'n'
' rethrow(e, __lines, __filename, __line, escapeFn);' 'n'
'}' 'n';
}
else {
src = this.source;
}
if (opts.client) {
src = 'escapeFn = escapeFn || ' escapeFn.toString() ';' 'n' src;
if (opts.compileDebug) {
src = 'rethrow = rethrow || ' rethrow.toString() ';' 'n' src;
}
}
if (opts.strict) {
src = '"use strict";n' src;
}
if (opts.debug) {
console.log(src);
}
if (opts.compileDebug && opts.filename) {
src = src 'n'
'//# sourceURL=' sanitizedFilename 'n';
}
try {
if (opts.async) {
try {
ctor = (new Function('return (async function(){}).constructor;'))();
}
catch(e) {
if (e instanceof SyntaxError) {
throw new Error('This environment does not support async/await');
}
else {
throw e;
}
}
}
else {
ctor = Function;
}
fn = new ctor(opts.localsName ', escapeFn, include, rethrow', src);
}
catch(e) {
// istanbul ignore else
if (e instanceof SyntaxError) {
if (opts.filename) {
e.message = ' in ' opts.filename;
}
e.message = ' while compiling ejsnn';
e.message = 'If the above error is not helpful, you may want to try EJS-Lint:n';
e.message = 'https://github.com/RyanZim/EJS-Lint';
if (!opts.async) {
e.message = 'n';
e.message = 'Or, if you meant to create an async function, pass `async: true` as an option.';
}
}
throw e;
}
var returnedFn = opts.client ? fn : function anonymous(data) {
var include = function (path, includeData) {
var d = utils.shallowCopy({}, data);
if (includeData) {
d = utils.shallowCopy(d, includeData);
}
return includeFile(path, opts)(d);
};
return fn.apply(opts.context, [data || {}, escapeFn, include, rethrow]);
};
if (opts.filename && typeof Object.defineProperty === 'function') {
var filename = opts.filename;
var basename = path.basename(filename, path.extname(filename));
try {
Object.defineProperty(returnedFn, 'name', {
value: basename,
writable: false,
enumerable: false,
configurable: true
});
} catch (e) {/* ignore */}
}
return returnedFn;
},
generateSource:(最终拿到结果this.source)
代码语言:javascript复制generateSource: function () {
var opts = this.opts;
// Slurp spaces and tabs before <%_ and after _%>
this.templateText =
this.templateText.replace(/[ t]*<%_/gm, '<%_').replace(/_%>[ t]*/gm, '_%>');
var self = this;
var matches = this.parseTemplateText(); //⭐️⭐️⭐️⭐️⭐️
var d = this.opts.delimiter;
var o = this.opts.openDelimiter;
var c = this.opts.closeDelimiter;
if (matches && matches.length) {
matches.forEach(function (line, index) { //⭐️⭐️⭐️⭐️⭐️
var closing;
if ( line.indexOf(o d) === 0 // If it is a tag
&& line.indexOf(o d d) !== 0) { // and is not escaped
closing = matches[index 2];
if (!(closing == d c || closing == '-' d c || closing == '_' d c)) {
throw new Error('Could not find matching close tag for "' line '".');
}
}
self.scanLine(line); ////⭐️⭐️⭐️⭐️⭐️
});
}
},
7-3 动态生成Function with用法讲解
代码语言:javascript复制上一节代码没有继续追踪,根据自己的源码一步一步调试,生一节调试到的代码为:
// ejs.js line662
fn = new ctor(opts.localsName ', escapeFn, include, rethrow', src);
代码语言:javascript复制代码讲解: const ctor = Function; const fn = new ctor(‘a,b’,‘console.log(a,b)’) fn(1,2)
我们回到7-1节中基础代码,在optons加入参数debug为true,控制台输出内容为:
var __line = 1
, __lines = "<div><%= user.name%></div>"
, __filename = undefined;
try {
var __output = "";
function __append(s) { if (s !== undefined && s !== null) __output = s }
with (locals || {}) {
; __append("<div>")
; __append(escapeFn( user.name))
; __append("</div>")
}
return __output;
} catch (e) {
rethrow(e, __lines, __filename, __line, escapeFn);
}
代码语言:javascript复制通过代码,我们看到了‘with’,现在前端with的使用已经很不常见且不推荐使用了,这里简单了解下:
const ctx = {
user:{
name:'liugezhou'
}
}
with(ctx){
console.log(user.name)
}
7-4 ejs compile函数执行流程分析
代码语言:javascript复制apply简要解释
function test(a,b,c){
console.log(a,b,c)
console.log(this.a)
}
test(1,2,3) //通常调用 // 1 2 3
test.apply({a:'applt'},[2,3,4]) // 2 3 4
test.call({a:'call',2,3,4) // 2 3 4
7-5 ejs.render和renderFile原理讲解
ejs.render的代码执行流程为:
- const renderTemplate = ejs.render(html,data,options)
- exports.render ==> handleCache(opts, template)
- handleCache ==> return exports.compile(template, options);
- handleCache(opts, template)(data)
renderFile的原理讲解
- const renderFile = ejs.renderFile(path.resolve(__dirname,‘template.html’),data,options)
- exports.renderFile
- tryHandleCache(opts, data, cb)
- handleCache(options)(data)
第八章 加餐:require源码解析,彻底搞懂 npm 模块加载原理
8-1 require源码执行流程分析
- require使用场景
**
- 加载模块类型
- 加载内置模块: require(‘fs’)
- 加载node_modules模块:require(‘ejs’)
- 加载本地模块:require(‘./utils’)
- 支持加载文件
- js
- json
- node
- mjs
- 加载其它类型
- require执行流程
我们在调试这行代码的时候,在执行栈中可以看到,之前也执行了很多代码,这里的流程以及上面分析的使用场景,我们可以先引出一些思考:
- CommonJS模块的加载流程
- require如何加载内置模块? loadNativeModule
- require如何加载node_modules模块?
- require为什么会将非js/json/node文件视为js进行加载
- require源码
- 我们从 require(‘./ejs’) 这行代码在webStorm中开始调试。(点击step into )
- 打开 Scripts --> no domain --> internal --> modules --> cjs --> helpers.js
- return mod.require(path); ----> line of 77 [helpers.js]
- 这里的mod就是指Module对象,调试后每个字段含义为:
- id:源码文件路径
- path:源码文件对应的文件夹,通过path.dirname(id)生成
- exports:模块输出的内容,默认为{}
- parent:父模块信息
- filename:源码文件路径
- loaded:是否已经加载完毕
- children:子模块对象集合
- paths:模块查询范围
- 继续step into到下一步,进去Module对象的require方法
- 代码如下: (校验参数为 string类型且不为空)
Module.prototype.require = function(id) {
validateString(id, 'id');
if (id === '') {
throw new ERR_INVALID_ARG_VALUE('id', id,
'must be a non-empty string');
}
requireDepth ;
try {
return Module._load(id, this, /* isMain */ false);
} finally {
requireDepth--;
}
};
- Module._load(id,this,false) :
- id:传入的字符串
- this:Module对象
- isMain:flase表示加载的不是一个主模块
Module._load = function(request, parent, isMain) {
let relResolveCacheIdentifier;
if (parent) {
debug('Module._load REQUEST %s parent: %s', request, parent.id);
relResolveCacheIdentifier = `${parent.path}x00${request}`;
const filename = relativeResolveCache[relResolveCacheIdentifier];
if (filename !== undefined) {
const cachedModule = Module._cache[filename];
if (cachedModule !== undefined) {
updateChildren(parent, cachedModule, true);
return cachedModule.exports;
}
delete relativeResolveCache[relResolveCacheIdentifier];
}
}
// ✨✨✨
// Module._resolveFilename是require.resolve()的核心实现,在lerna源码讲解时学过--> Module._resolveLookupPaths()
const filename = Module._resolveFilename(request, parent, isMain);
const cachedModule = Module._cache[filename];
if (cachedModule !== undefined) {
updateChildren(parent, cachedModule, true);
return cachedModule.exports;
}
//✨✨✨
// loadNativeModule 中 加载内置模块,进入该源码:通过NativeModule.map我们可以看到所有的内置模块
const mod = loadNativeModule(filename, request, experimentalModules);
if (mod && mod.canBeRequiredByUsers) return mod.exports;
// 不是内置模块,new Module,其中children在new的时候完成
const module = new Module(filename, parent);
if (isMain) {
process.mainModule = module;
module.id = '.';
}
Module._cache[filename] = module;
if (parent !== undefined) {
relativeResolveCache[relResolveCacheIdentifier] = filename;
}
let threw = true;
try {
if (enableSourceMaps) {
try {
module.load(filename);
} catch (err) {
rekeySourceMap(Module._cache[filename], err);
throw err; /* node-do-not-add-exception-line */
}
} else {
//